Merge changes I7bffeb56,I945e5086,Ic801c654,I50c54322 into udc-qpr-dev

* changes:
  [Decor] Remove the now-unused main executor
  [Decor] Support ScreenDecorCommand in ScreenDecorations
  [ScreenDecor CLI] ScreenDecorCommand based on ParseableCommand
  [CommandLine] CLI parsing lib to work with Command
This commit is contained in:
Evan Laird
2023-06-29 03:46:36 +00:00
committed by Android (Google) Code Review
14 changed files with 2120 additions and 21 deletions

View File

@@ -56,9 +56,14 @@ class ScreenDecorHwcLayer(
) : DisplayCutoutBaseView(context) {
val colorMode: Int
private val useInvertedAlphaColor: Boolean
private val color: Int
private var color: Int = Color.BLACK
set(value) {
field = value
paint.color = value
}
private val bgColor: Int
private val cornerFilter: ColorFilter
private var cornerFilter: ColorFilter
private val cornerBgFilter: ColorFilter
private val clearPaint: Paint
@JvmField val transparentRect: Rect = Rect()
@@ -109,10 +114,16 @@ class ScreenDecorHwcLayer(
override fun onAttachedToWindow() {
super.onAttachedToWindow()
parent.requestTransparentRegion(this)
updateColors()
}
private fun updateColors() {
if (!debug) {
viewRootImpl.setDisplayDecoration(true)
}
cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
if (useInvertedAlphaColor) {
paint.set(clearPaint)
} else {
@@ -121,6 +132,21 @@ class ScreenDecorHwcLayer(
}
}
fun setDebugColor(color: Int) {
if (!debug) {
return
}
if (this.color == color) {
return
}
this.color = color
updateColors()
invalidate()
}
override fun onUpdate() {
parent.requestTransparentRegion(this)
}
@@ -367,7 +393,7 @@ class ScreenDecorHwcLayer(
/**
* Update the rounded corner drawables.
*/
fun updateRoundedCornerDrawable(top: Drawable, bottom: Drawable) {
fun updateRoundedCornerDrawable(top: Drawable?, bottom: Drawable?) {
roundedCornerDrawableTop = top
roundedCornerDrawableBottom = bottom
updateRoundedCornerDrawableBounds()

View File

@@ -69,9 +69,9 @@ import com.android.internal.util.Preconditions;
import com.android.settingslib.Utils;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
import com.android.systemui.decor.DebugRoundedCornerDelegate;
import com.android.systemui.decor.DebugRoundedCornerModel;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.DecorProviderKt;
@@ -80,10 +80,12 @@ import com.android.systemui.decor.OverlayWindow;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegateImpl;
import com.android.systemui.decor.ScreenDecorCommand;
import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.qs.SettingObserver;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.ThreadFactory;
@@ -95,7 +97,6 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import javax.inject.Inject;
@@ -130,7 +131,7 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
@VisibleForTesting
protected boolean mIsRegistered;
private final Context mContext;
private final Executor mMainExecutor;
private final CommandRegistry mCommandRegistry;
private final SecureSettings mSecureSettings;
@VisibleForTesting
DisplayTracker.Callback mDisplayListener;
@@ -313,8 +314,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
@Inject
public ScreenDecorations(Context context,
@Main Executor mainExecutor,
SecureSettings secureSettings,
CommandRegistry commandRegistry,
UserTracker userTracker,
DisplayTracker displayTracker,
PrivacyDotViewController dotViewController,
@@ -324,8 +325,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
ScreenDecorationsLogger logger,
AuthController authController) {
mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mCommandRegistry = commandRegistry;
mUserTracker = userTracker;
mDisplayTracker = displayTracker;
mDotViewController = dotViewController;
@@ -350,6 +351,45 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
}
};
private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> {
// If we are exiting debug mode, we can set it (false) and bail, otherwise we will
// ensure that debug mode is set
if (cmd.getDebug() != null && !cmd.getDebug()) {
setDebug(false);
return;
} else {
// setDebug is idempotent
setDebug(true);
}
if (cmd.getColor() != null) {
mDebugColor = cmd.getColor();
mExecutor.execute(() -> {
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.setDebugColor(cmd.getColor());
}
updateColorInversionDefault();
});
}
DebugRoundedCornerModel roundedTop = null;
DebugRoundedCornerModel roundedBottom = null;
if (cmd.getRoundedTop() != null) {
roundedTop = cmd.getRoundedTop().toRoundedCornerDebugModel();
}
if (cmd.getRoundedBottom() != null) {
roundedBottom = cmd.getRoundedBottom().toRoundedCornerDebugModel();
}
if (roundedTop != null || roundedBottom != null) {
mDebugRoundedCornerDelegate.applyNewDebugCorners(roundedTop, roundedBottom);
mExecutor.execute(() -> {
removeAllOverlays();
removeHwcOverlay();
setupDecorations();
});
}
};
@Override
public void start() {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -361,6 +401,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
mExecutor.execute(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mExecutor);
mAuthController.addCallback(mAuthControllerCallback);
mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME,
() -> new ScreenDecorCommand(mScreenDecorCommandCallback));
}
/**
@@ -1228,7 +1270,7 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
bottomDrawable = mDebugRoundedCornerDelegate.getBottomRoundedDrawable();
}
if (topDrawable == null || bottomDrawable == null) {
if (topDrawable == null && bottomDrawable == null) {
return;
}
mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);

View File

@@ -77,16 +77,30 @@ class DebugRoundedCornerDelegate : RoundedCornerResDelegate {
}
fun applyNewDebugCorners(
topCorner: DebugRoundedCornerModel,
bottomCorner: DebugRoundedCornerModel,
topCorner: DebugRoundedCornerModel?,
bottomCorner: DebugRoundedCornerModel?,
) {
hasTop = true
topRoundedDrawable = topCorner.toPathDrawable(paint)
topRoundedSize = topCorner.size()
topCorner?.let {
hasTop = true
topRoundedDrawable = it.toPathDrawable(paint)
topRoundedSize = it.size()
}
?: {
hasTop = false
topRoundedDrawable = null
topRoundedSize = Size(0, 0)
}
hasBottom = true
bottomRoundedDrawable = bottomCorner.toPathDrawable(paint)
bottomRoundedSize = bottomCorner.size()
bottomCorner?.let {
hasBottom = true
bottomRoundedDrawable = it.toPathDrawable(paint)
bottomRoundedSize = it.size()
}
?: {
hasBottom = false
bottomRoundedDrawable = null
bottomRoundedSize = Size(0, 0)
}
}
/**

View File

@@ -0,0 +1,171 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.decor
import android.graphics.Color
import android.graphics.Path
import android.util.PathParser
import com.android.systemui.statusbar.commandline.ParseableCommand
import com.android.systemui.statusbar.commandline.Type
import com.android.systemui.statusbar.commandline.map
import java.io.PrintWriter
/** Debug screen-decor command to be handled by the SystemUI command line interface */
class ScreenDecorCommand(
private val callback: Callback,
) : ParseableCommand(SCREEN_DECOR_CMD_NAME) {
val debug: Boolean? by
param(
longName = "debug",
description =
"Enter or exits debug mode. Effectively makes the corners visible and allows " +
"for overriding the path data for the anti-aliasing corner paths and display " +
"cutout.",
valueParser = Type.Boolean,
)
val color: Int? by
param(
longName = "color",
shortName = "c",
description =
"Set a specific color for the debug assets. See Color#parseString() for " +
"accepted inputs.",
valueParser = Type.String.map { it.toColorIntOrNull() }
)
val roundedTop: RoundedCornerSubCommand? by subCommand(RoundedCornerSubCommand("rounded-top"))
val roundedBottom: RoundedCornerSubCommand? by
subCommand(RoundedCornerSubCommand("rounded-bottom"))
override fun execute(pw: PrintWriter) {
callback.onExecute(this, pw)
}
override fun toString(): String {
return "ScreenDecorCommand(" +
"debug=$debug, " +
"color=$color, " +
"roundedTop=$roundedTop, " +
"roundedBottom=$roundedBottom)"
}
/** For use in ScreenDecorations.java, define a Callback */
interface Callback {
fun onExecute(cmd: ScreenDecorCommand, pw: PrintWriter)
}
companion object {
const val SCREEN_DECOR_CMD_NAME = "screen-decor"
}
}
/**
* Defines a subcommand suitable for `rounded-top` and `rounded-bottom`. They both have the same
* API.
*/
class RoundedCornerSubCommand(name: String) : ParseableCommand(name) {
val height by
param(
longName = "height",
description = "The height of a corner, in pixels.",
valueParser = Type.Int,
)
.required()
val width by
param(
longName = "width",
description =
"The width of the corner, in pixels. Likely should be equal to the height.",
valueParser = Type.Int,
)
.required()
val pathData by
param(
longName = "path-data",
shortName = "d",
description =
"PathParser-compatible path string to be rendered as the corner drawable. " +
"This path should be a closed arc oriented as the top-left corner " +
"of the device",
valueParser = Type.String.map { it.toPathOrNull() }
)
.required()
val viewportHeight: Float? by
param(
longName = "viewport-height",
description =
"The height of the viewport for the given path string. " +
"If null, the corner height will be used.",
valueParser = Type.Float,
)
val scaleY: Float
get() = viewportHeight?.let { height.toFloat() / it } ?: 1.0f
val viewportWidth: Float? by
param(
longName = "viewport-width",
description =
"The width of the viewport for the given path string. " +
"If null, the corner width will be used.",
valueParser = Type.Float,
)
val scaleX: Float
get() = viewportWidth?.let { width.toFloat() / it } ?: 1.0f
override fun execute(pw: PrintWriter) {
// Not needed for a subcommand
}
override fun toString(): String {
return "RoundedCornerSubCommand(" +
"height=$height," +
" width=$width," +
" pathData='$pathData'," +
" viewportHeight=$viewportHeight," +
" viewportWidth=$viewportWidth)"
}
fun toRoundedCornerDebugModel(): DebugRoundedCornerModel =
DebugRoundedCornerModel(
path = pathData,
width = width,
height = height,
scaleX = scaleX,
scaleY = scaleY,
)
}
fun String.toPathOrNull(): Path? =
try {
PathParser.createPathFromPathData(this)
} catch (e: Exception) {
null
}
fun String.toColorIntOrNull(): Int? =
try {
Color.parseColor(this)
} catch (e: Exception) {
null
}

View File

@@ -0,0 +1,327 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
/**
* [CommandParser] defines the collection of tokens which can be parsed from an incoming command
* list, and parses them into their respective containers. Supported tokens are of the following
* forms:
* ```
* Flag: boolean value, false by default. always optional.
* Param: named parameter, taking N args all of a given type. Currently only single arg parameters
* are supported.
* SubCommand: named command created by adding a command to a parent. Supports all fields above, but
* not other subcommands.
* ```
*
* Tokens are added via the factory methods for each token type. They can be made `required` by
* calling the [require] method for the appropriate type, as follows:
* ```
* val requiredParam = parser.require(parser.param(...))
* ```
*
* The reason for having an explicit require is so that generic type arguments can be handled
* properly. See [SingleArgParam] and [SingleArgParamOptional] for the difference between an
* optional parameter and a required one.
*
* Typical usage of a required parameter, however, will occur within the context of a
* [ParseableCommand], which defines a convenience `require()` method:
* ```
* class MyCommand : ParseableCommand {
* val requiredParam = param(...).require()
* }
* ```
*
* This parser defines two modes of parsing, both of which validate for required parameters.
* 1. [parse] is a top-level parsing method. This parser will walk the given arg list and populate
* all of the delegate classes based on their type. It will handle SubCommands, and after parsing
* will check for any required-but-missing SubCommands or Params.
*
* **This method requires that every received token is represented in its grammar.**
* 2. [parseAsSubCommand] is a second-level parsing method suitable for any [SubCommand]. This
* method will handle _only_ flags and params. It will return parsing control to its parent
* parser on the first unknown token rather than throwing.
*/
class CommandParser {
private val _flags = mutableListOf<Flag>()
val flags: List<Flag> = _flags
private val _params = mutableListOf<Param>()
val params: List<Param> = _params
private val _subCommands = mutableListOf<SubCommand>()
val subCommands: List<SubCommand> = _subCommands
private val tokenSet = mutableSetOf<String>()
/**
* Parse the arg list into the fields defined in the containing class.
*
* @return true if all required fields are present after parsing
* @throws ArgParseError on any failure to process args
*/
fun parse(args: List<String>): Boolean {
if (args.isEmpty()) {
return false
}
val iterator = args.listIterator()
var tokenHandled: Boolean
while (iterator.hasNext()) {
val token = iterator.next()
tokenHandled = false
flags
.find { it.matches(token) }
?.let {
it.inner = true
tokenHandled = true
}
if (tokenHandled) continue
params
.find { it.matches(token) }
?.let {
it.parseArgsFromIter(iterator)
tokenHandled = true
}
if (tokenHandled) continue
subCommands
.find { it.matches(token) }
?.let {
it.parseSubCommandArgs(iterator)
tokenHandled = true
}
if (!tokenHandled) {
throw ArgParseError("Unknown token: $token")
}
}
return validateRequiredParams()
}
/**
* Parse a subset of the commands that came in from the top-level [parse] method, for the
* subcommand that this parser represents. Note that subcommands may not contain other
* subcommands. But they may contain flags and params.
*
* @return true if all required fields are present after parsing
* @throws ArgParseError on any failure to process args
*/
fun parseAsSubCommand(iter: ListIterator<String>): Boolean {
// arg[-1] is our subcommand name, so the rest of the args are either for this
// subcommand, OR for the top-level command to handle. Therefore, we bail on the first
// failure, but still check our own required params
// The mere presence of a subcommand (similar to a flag) is a valid subcommand
if (flags.isEmpty() && params.isEmpty()) {
return validateRequiredParams()
}
var tokenHandled: Boolean
while (iter.hasNext()) {
val token = iter.next()
tokenHandled = false
flags
.find { it.matches(token) }
?.let {
it.inner = true
tokenHandled = true
}
if (tokenHandled) continue
params
.find { it.matches(token) }
?.let {
it.parseArgsFromIter(iter)
tokenHandled = true
}
if (!tokenHandled) {
// Move the cursor position backwards since we've arrived at a token
// that we don't own
iter.previous()
break
}
}
return validateRequiredParams()
}
/**
* If [parse] or [parseAsSubCommand] does not produce a valid result, generate a list of errors
* based on missing elements
*/
fun generateValidationErrorMessages(): List<String> {
val missingElements = mutableListOf<String>()
if (unhandledParams.isNotEmpty()) {
val names = unhandledParams.map { it.longName }
missingElements.add("No values passed for required params: $names")
}
if (unhandledSubCmds.isNotEmpty()) {
missingElements.addAll(unhandledSubCmds.map { it.longName })
val names = unhandledSubCmds.map { it.shortName }
missingElements.add("No values passed for required sub-commands: $names")
}
return missingElements
}
/** Check for any missing, required params, or any invalid subcommands */
private fun validateRequiredParams(): Boolean =
unhandledParams.isEmpty() && unhandledSubCmds.isEmpty() && unvalidatedSubCmds.isEmpty()
// If any required param (aka non-optional) hasn't handled a field, then return false
private val unhandledParams: List<Param>
get() = params.filter { (it is SingleArgParam<*>) && !it.handled }
private val unhandledSubCmds: List<SubCommand>
get() = subCommands.filter { (it is RequiredSubCommand<*> && !it.handled) }
private val unvalidatedSubCmds: List<SubCommand>
get() = subCommands.filter { !it.validationStatus }
private fun checkCliNames(short: String?, long: String): String? {
if (short != null && tokenSet.contains(short)) {
return short
}
if (tokenSet.contains(long)) {
return long
}
return null
}
private fun subCommandContainsSubCommands(cmd: ParseableCommand): Boolean =
cmd.parser.subCommands.isNotEmpty()
private fun registerNames(short: String?, long: String) {
if (short != null) {
tokenSet.add(short)
}
tokenSet.add(long)
}
/**
* Turns a [SingleArgParamOptional]<T> into a [SingleArgParam] by converting the [T?] into [T]
*
* @return a [SingleArgParam] property delegate
*/
fun <T : Any> require(old: SingleArgParamOptional<T>): SingleArgParam<T> {
val newParam =
SingleArgParam(
longName = old.longName,
shortName = old.shortName,
description = old.description,
valueParser = old.valueParser,
)
replaceWithRequired(old, newParam)
return newParam
}
private fun <T : Any> replaceWithRequired(
old: SingleArgParamOptional<T>,
new: SingleArgParam<T>,
) {
_params.remove(old)
_params.add(new)
}
/**
* Turns an [OptionalSubCommand] into a [RequiredSubCommand] by converting the [T?] in to [T]
*
* @return a [RequiredSubCommand] property delegate
*/
fun <T : ParseableCommand> require(optional: OptionalSubCommand<T>): RequiredSubCommand<T> {
val newCmd = RequiredSubCommand(optional.cmd)
replaceWithRequired(optional, newCmd)
return newCmd
}
private fun <T : ParseableCommand> replaceWithRequired(
old: OptionalSubCommand<T>,
new: RequiredSubCommand<T>,
) {
_subCommands.remove(old)
_subCommands.add(new)
}
internal fun flag(
longName: String,
shortName: String? = null,
description: String = "",
): Flag {
checkCliNames(shortName, longName)?.let {
throw IllegalArgumentException("Detected reused flag name ($it)")
}
registerNames(shortName, longName)
val flag = Flag(shortName, longName, description)
_flags.add(flag)
return flag
}
internal fun <T : Any> param(
longName: String,
shortName: String? = null,
description: String = "",
valueParser: ValueParser<T>,
): SingleArgParamOptional<T> {
checkCliNames(shortName, longName)?.let {
throw IllegalArgumentException("Detected reused param name ($it)")
}
registerNames(shortName, longName)
val param =
SingleArgParamOptional(
shortName = shortName,
longName = longName,
description = description,
valueParser = valueParser,
)
_params.add(param)
return param
}
internal fun <T : ParseableCommand> subCommand(
command: T,
): OptionalSubCommand<T> {
checkCliNames(null, command.name)?.let {
throw IllegalArgumentException("Cannot re-use name for subcommand ($it)")
}
if (subCommandContainsSubCommands(command)) {
throw IllegalArgumentException(
"SubCommands may not contain other SubCommands. $command"
)
}
registerNames(null, command.name)
val subCmd = OptionalSubCommand(command)
_subCommands.add(subCmd)
return subCmd
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import android.util.IndentingPrintWriter
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* Definitions for all parameter types usable by [ParseableCommand]. Parameters are command line
* tokens that accept a fixed number of arguments and convert them to a parsed type.
*
* Example:
* ```
* my_command --single-arg-param arg
* ```
*
* In the example, `my_command` is the name of the command, `--single-arg-param` is the parameter,
* and `arg` is the value parsed by that parameter into its eventual type.
*
* Note on generics: The intended usage for parameters is to be able to return the parsed type from
* the given command as a `val` via property delegation. For example, let's say we have a command
* that has one optional and one required parameter:
* ```
* class MyCommand : ParseableCommand {
* val requiredParam: Int by parser.param(...).required()
* val optionalParam: Int? by parser.param(...)
* }
* ```
*
* In order to make the simple `param` method return the correct type, we need to do two things:
* 1. Break out the generic type into 2 pieces (TParsed and T)
* 2. Create two different underlying Parameter subclasses to handle the property delegation. One
* handles `T?` and the other handles `T`. Note that in both cases, `TParsed` is always non-null
* since the value parsed from the argument will throw an exception if missing or if it cannot be
* parsed.
*/
/** A param type knows the number of arguments it expects */
sealed interface Param : Describable {
val numArgs: Int
/**
* Consume [numArgs] items from the iterator and relay the result into its corresponding
* delegated type.
*/
fun parseArgsFromIter(iterator: Iterator<String>)
}
/**
* Base class for required and optional SingleArgParam classes. For convenience, UnaryParam is
* defined as a [MultipleArgParam] where numArgs = 1. The benefit is that we can define the parsing
* in a single place, and yet on the client side we can unwrap the underlying list of params
* automatically.
*/
abstract class UnaryParamBase<out T, out TParsed : T>(val wrapped: MultipleArgParam<T, TParsed>) :
Param, ReadOnlyProperty<Any?, T> {
var handled = false
override fun describe(pw: IndentingPrintWriter) {
if (shortName != null) {
pw.print("$shortName, ")
}
pw.print(longName)
pw.println(" ${typeDescription()}")
if (description != null) {
pw.indented { pw.println(description) }
}
}
/**
* Try to describe the arg type. We can know if it's one of the base types what kind of input it
* takes. Otherwise just print "<arg>" and let the clients describe in the help text
*/
private fun typeDescription() =
when (wrapped.valueParser) {
Type.Int -> "<int>"
Type.Float -> "<float>"
Type.String -> "<string>"
Type.Boolean -> "<boolean>"
else -> "<arg>"
}
}
/** Required single-arg parameter, delegating a non-null type to the client. */
class SingleArgParam<out T : Any>(
override val longName: String,
override val shortName: String? = null,
override val description: String? = null,
val valueParser: ValueParser<T>,
) :
UnaryParamBase<T, T>(
MultipleArgParam(
longName,
shortName,
1,
description,
valueParser,
)
) {
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
if (handled) {
wrapped.getValue(thisRef, property)[0]
} else {
throw IllegalStateException("Attempt to read property before parse() has executed")
}
override val numArgs: Int = 1
override fun parseArgsFromIter(iterator: Iterator<String>) {
wrapped.parseArgsFromIter(iterator)
handled = true
}
}
/** Optional single-argument parameter, delegating a nullable type to the client. */
class SingleArgParamOptional<out T : Any>(
override val longName: String,
override val shortName: String? = null,
override val description: String? = null,
val valueParser: ValueParser<T>,
) :
UnaryParamBase<T?, T>(
MultipleArgParam(
longName,
shortName,
1,
description,
valueParser,
)
) {
override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
wrapped.getValue(thisRef, property).getOrNull(0)
override val numArgs: Int = 1
override fun parseArgsFromIter(iterator: Iterator<String>) {
wrapped.parseArgsFromIter(iterator)
handled = true
}
}
/**
* Parses a list of args into the underlying [T] data type. The resultant value is an ordered list
* of type [TParsed].
*
* [T] and [TParsed] are split out here in the case where the entire param is optional. I.e., a
* MultipleArgParam<T?, T> indicates a command line argument that can be omitted. In that case, the
* inner list is List<T>?, NOT List<T?>. If the argument is provided, then the type is always going
* to be parsed into T rather than T?.
*/
class MultipleArgParam<out T, out TParsed : T>(
override val longName: String,
override val shortName: String? = null,
override val numArgs: Int = 1,
override val description: String? = null,
val valueParser: ValueParser<TParsed>,
) : ReadOnlyProperty<Any?, List<TParsed>>, Param {
private val inner: MutableList<TParsed> = mutableListOf()
override fun getValue(thisRef: Any?, property: KProperty<*>): List<TParsed> = inner
/**
* Consumes [numArgs] values of the iterator and parses them into [TParsed].
*
* @throws ArgParseError on the first failure
*/
override fun parseArgsFromIter(iterator: Iterator<String>) {
if (!iterator.hasNext()) {
throw ArgParseError("no argument provided for $shortName")
}
for (i in 0 until numArgs) {
valueParser
.parseValue(iterator.next())
.fold(onSuccess = { inner.add(it) }, onFailure = { throw it })
}
}
}
data class ArgParseError(override val message: String) : Exception(message)

View File

@@ -0,0 +1,395 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import android.util.IndentingPrintWriter
import java.io.PrintWriter
import java.lang.IllegalArgumentException
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* An implementation of [Command] that includes a [CommandParser] which can set all delegated
* properties.
*
* As the number of registrants to [CommandRegistry] grows, we should have a default mechanism for
* parsing common command line arguments. We are not expecting to build an arbitrarily-functional
* CLI, nor a GNU arg parse compliant interface here, we simply want to be able to empower clients
* to create simple CLI grammars such as:
* ```
* $ my_command [-f|--flag]
* $ my_command [-a|--arg] <params...>
* $ my_command [subcommand1] [subcommand2]
* $ my_command <positional_arg ...> # not-yet implemented
* ```
*
* Note that the flags `-h` and `--help` are reserved for the base class. It seems prudent to just
* avoid them in your implementation.
*
* Usage:
*
* The intended usage tries to be clever enough to enable good ergonomics, while not too clever as
* to be unmaintainable. Using the default parser is done using property delegates, and looks like:
* ```
* class MyCommand(
* onExecute: (cmd: MyCommand, pw: PrintWriter) -> ()
* ) : ParseableCommand(name) {
* val flag1 by flag(
* shortName = "-f",
* longName = "--flag",
* required = false,
* )
* val param1: String by param(
* shortName = "-a",
* longName = "--args",
* valueParser = Type.String
* ).required()
* val param2: Int by param(..., valueParser = Type.Int)
* val subCommand by subCommand(...)
*
* override fun execute(pw: PrintWriter) {
* onExecute(this, pw)
* }
*
* companion object {
* const val name = "my_command"
* }
* }
*
* fun main() {
* fun printArgs(cmd: MyCommand, pw: PrintWriter) {
* pw.println("${cmd.flag1}")
* pw.println("${cmd.param1}")
* pw.println("${cmd.param2}")
* pw.println("${cmd.subCommand}")
* }
*
* commandRegistry.registerCommand(MyCommand.companion.name) {
* MyCommand() { (cmd, pw) ->
* printArgs(cmd, pw)
* }
* }
* }
*
* ```
*/
abstract class ParseableCommand(val name: String, val description: String? = null) : Command {
val parser: CommandParser = CommandParser()
val help by flag(longName = "help", shortName = "h", description = "Print help and return")
/**
* After [execute(pw, args)] is called, this class goes through a parsing stage and sets all
* delegated properties. It is safe to read any delegated properties here.
*
* This method is never called for [SubCommand]s, since they are associated with a top-level
* command that handles [execute]
*/
abstract fun execute(pw: PrintWriter)
/**
* Given a command string list, [execute] parses the incoming command and validates the input.
* If this command or any of its subcommands is passed `-h` or `--help`, then execute will only
* print the relevant help message and exit.
*
* If any error is thrown during parsing, we will catch and log the error. This process should
* _never_ take down its process. Override [onParseFailed] to handle an [ArgParseError].
*
* Important: none of the delegated fields can be read before this stage.
*/
override fun execute(pw: PrintWriter, args: List<String>) {
val success: Boolean
try {
success = parser.parse(args)
} catch (e: ArgParseError) {
pw.println(e.message)
onParseFailed(e)
return
} catch (e: Exception) {
pw.println("Unknown exception encountered during parse")
pw.println(e)
return
}
// Now we've parsed the incoming command without error. There are two things to check:
// 1. If any help is requested, print the help message and return
// 2. Otherwise, make sure required params have been passed in, and execute
val helpSubCmds = subCmdsRequestingHelp()
// Top-level help encapsulates subcommands. Otherwise, if _any_ subcommand requests
// help then defer to them. Else, just execute
if (help) {
help(pw)
} else if (helpSubCmds.isNotEmpty()) {
helpSubCmds.forEach { it.help(pw) }
} else {
if (!success) {
parser.generateValidationErrorMessages().forEach { pw.println(it) }
} else {
execute(pw)
}
}
}
/**
* Returns a list of all commands that asked for help. If non-empty, parsing will stop to print
* help. It is not guaranteed that delegates are fulfilled if help is requested
*/
private fun subCmdsRequestingHelp(): List<ParseableCommand> =
parser.subCommands.filter { it.cmd.help }.map { it.cmd }
/** Override to do something when parsing fails */
open fun onParseFailed(error: ArgParseError) {}
/** Override to print a usage clause. E.g. `usage: my-cmd <arg1> <arg2>` */
open fun usage(pw: IndentingPrintWriter) {}
/**
* Print out the list of tokens, their received types if any, and their description in a
* formatted string.
*
* Example:
* ```
* my-command:
* MyCmd.description
*
* [optional] usage block
*
* Flags:
* -f
* description
* --flag2
* description
*
* Parameters:
* Required:
* -p1 [Param.Type]
* description
* --param2 [Param.Type]
* description
* Optional:
* same as above
*
* SubCommands:
* Required:
* ...
* Optional:
* ...
* ```
*/
override fun help(pw: PrintWriter) {
val ipw = IndentingPrintWriter(pw)
ipw.printBoxed(name)
ipw.println()
// Allow for a simple `usage` block for clients
ipw.indented { usage(ipw) }
if (description != null) {
ipw.indented { ipw.println(description) }
ipw.println()
}
val flags = parser.flags
if (flags.isNotEmpty()) {
ipw.println("FLAGS:")
ipw.indented {
flags.forEach {
it.describe(ipw)
ipw.println()
}
}
}
val (required, optional) = parser.params.partition { it is SingleArgParam<*> }
if (required.isNotEmpty()) {
ipw.println("REQUIRED PARAMS:")
required.describe(ipw)
}
if (optional.isNotEmpty()) {
ipw.println("OPTIONAL PARAMS:")
optional.describe(ipw)
}
val (reqSub, optSub) = parser.subCommands.partition { it is RequiredSubCommand<*> }
if (reqSub.isNotEmpty()) {
ipw.println("REQUIRED SUBCOMMANDS:")
reqSub.describe(ipw)
}
if (optSub.isNotEmpty()) {
ipw.println("OPTIONAL SUBCOMMANDS:")
optSub.describe(ipw)
}
}
fun flag(
longName: String,
shortName: String? = null,
description: String = "",
): Flag {
if (!checkShortName(shortName)) {
throw IllegalArgumentException(
"Flag short name must be one character long, or null. Got ($shortName)"
)
}
if (!checkLongName(longName)) {
throw IllegalArgumentException("Flags must not start with '-'. Got $($longName)")
}
val short = shortName?.let { "-$shortName" }
val long = "--$longName"
return parser.flag(long, short, description)
}
fun <T : Any> param(
longName: String,
shortName: String? = null,
description: String = "",
valueParser: ValueParser<T>,
): SingleArgParamOptional<T> {
if (!checkShortName(shortName)) {
throw IllegalArgumentException(
"Parameter short name must be one character long, or null. Got ($shortName)"
)
}
if (!checkLongName(longName)) {
throw IllegalArgumentException("Parameters must not start with '-'. Got $($longName)")
}
val short = shortName?.let { "-$shortName" }
val long = "--$longName"
return parser.param(long, short, description, valueParser)
}
fun <T : ParseableCommand> subCommand(
command: T,
) = parser.subCommand(command)
/** For use in conjunction with [param], makes the parameter required */
fun <T : Any> SingleArgParamOptional<T>.required(): SingleArgParam<T> = parser.require(this)
/** For use in conjunction with [subCommand], makes the given [SubCommand] required */
fun <T : ParseableCommand> OptionalSubCommand<T>.required(): RequiredSubCommand<T> =
parser.require(this)
private fun checkShortName(short: String?): Boolean {
return short == null || short.length == 1
}
private fun checkLongName(long: String): Boolean {
return !long.startsWith("-")
}
companion object {
fun Iterable<Describable>.describe(pw: IndentingPrintWriter) {
pw.indented {
forEach {
it.describe(pw)
pw.println()
}
}
}
}
}
/**
* A flag is a boolean value passed over the command line. It can have a short form or long form.
* The value is [Boolean.true] if the flag is found, else false
*/
data class Flag(
override val shortName: String? = null,
override val longName: String,
override val description: String? = null,
) : ReadOnlyProperty<Any?, Boolean>, Describable {
var inner: Boolean = false
override fun getValue(thisRef: Any?, property: KProperty<*>) = inner
}
/**
* Named CLI token. Can have a short or long name. Note: consider renaming to "primary" and
* "secondary" names since we don't actually care what the strings are
*
* Flags and params will have [shortName]s that are always prefixed with a single dash, while
* [longName]s are prefixed by a double dash. E.g., `my_command -f --flag`.
*
* Subcommands do not do any prefixing, and register their name as the [longName]
*
* Can be matched against an incoming token
*/
interface CliNamed {
val shortName: String?
val longName: String
fun matches(token: String) = shortName == token || longName == token
}
interface Describable : CliNamed {
val description: String?
fun describe(pw: IndentingPrintWriter) {
if (shortName != null) {
pw.print("$shortName, ")
}
pw.print(longName)
pw.println()
if (description != null) {
pw.indented { pw.println(description) }
}
}
}
/**
* Print [s] inside of a unicode character box, like so:
* ```
* ╔═══════════╗
* ║ my-string ║
* ╚═══════════╝
* ```
*/
fun PrintWriter.printDoubleBoxed(s: String) {
val length = s.length
println("╔${"═".repeat(length + 2)}╗")
println("║ $s ║")
println("╚${"═".repeat(length + 2)}╝")
}
/**
* Print [s] inside of a unicode character box, like so:
* ```
* ┌───────────┐
* │ my-string │
* └───────────┘
* ```
*/
fun PrintWriter.printBoxed(s: String) {
val length = s.length
println("┌${"─".repeat(length + 2)}┐")
println("│ $s │")
println("└${"─".repeat(length + 2)}┘")
}
fun IndentingPrintWriter.indented(block: () -> Unit) {
increaseIndent()
block()
decreaseIndent()
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import android.util.IndentingPrintWriter
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* Sub commands wrap [ParseableCommand]s and are attached to a parent [ParseableCommand]. As such
* they have their own parser which will parse the args as a subcommand. I.e., the subcommand's
* parser will consume the iterator created by the parent, reversing the index when it reaches an
* unknown token.
*
* In order to keep subcommands relatively simple and not have to do complicated validation, sub
* commands will return control to the parent parser as soon as they discover a token that they do
* not own. They will throw an [ArgParseError] if parsing fails or if they don't receive arguments
* for a required parameter.
*/
sealed interface SubCommand : Describable {
val cmd: ParseableCommand
/** Checks if all of the required elements were passed in to [parseSubCommandArgs] */
var validationStatus: Boolean
/**
* To keep parsing simple, [parseSubCommandArgs] requires a [ListIterator] so that it can rewind
* the iterator when it yields control upwards
*/
fun parseSubCommandArgs(iterator: ListIterator<String>)
}
/**
* Note that the delegated type from the subcommand is `T: ParseableCommand?`. SubCommands are
* created via adding a fully-formed [ParseableCommand] to parent command.
*
* At this point in time, I don't recommend nesting subcommands.
*/
class OptionalSubCommand<T : ParseableCommand>(
override val cmd: T,
) : SubCommand, ReadOnlyProperty<Any?, ParseableCommand?> {
override val shortName: String? = null
override val longName: String = cmd.name
override val description: String? = cmd.description
override var validationStatus = true
private var isPresent = false
/** Consume tokens from the iterator and pass them to the wrapped command */
override fun parseSubCommandArgs(iterator: ListIterator<String>) {
validationStatus = cmd.parser.parseAsSubCommand(iterator)
isPresent = true
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
if (isPresent) {
cmd
} else {
null
}
override fun describe(pw: IndentingPrintWriter) {
cmd.help(pw)
}
}
/**
* Non-optional subcommand impl. Top-level parser is expected to throw [ArgParseError] if this token
* is not present in the incoming command
*/
class RequiredSubCommand<T : ParseableCommand>(
override val cmd: T,
) : SubCommand, ReadOnlyProperty<Any?, ParseableCommand> {
override val shortName: String? = null
override val longName: String = cmd.name
override val description: String? = cmd.description
override var validationStatus = true
/** Unhandled, required subcommands are an error */
var handled = false
override fun parseSubCommandArgs(iterator: ListIterator<String>) {
validationStatus = cmd.parser.parseAsSubCommand(iterator)
handled = true
}
override fun getValue(thisRef: Any?, property: KProperty<*>): ParseableCommand = cmd
override fun describe(pw: IndentingPrintWriter) {
cmd.help(pw)
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Utilities for parsing the [String] command line arguments. Arguments are related to the
* [Parameter] type, which declares the number of, and resulting type of, the arguments that it
* takes when parsing. For Example:
* ```
* my-command --param <str> --param2 <int>
* ```
*
* Defines 2 parameters, the first of which takes a string, and the second requires an int. Because
* fundamentally _everything_ is a string, we have to define a convenient way to get from the
* incoming `StringArg` to the resulting `T`-arg, where `T` is the type required by the client.
*
* Parsing is therefore a relatively straightforward operation: (String) -> T. However, since
* parsing can always fail, the type is actually (String) -> Result<T>. We will always want to fail
* on the first error and propagate it to the caller (typically this results in printing the `help`
* message of the command`).
*
* The identity parsing is trivial:
* ```
* (s: String) -> String = { s -> s }
* ```
*
* Basic mappings are actually even provided by Kotlin's stdlib:
* ```
* (s: String) -> Boolean = { s -> s.toBooleanOrNull() }
* (s: String) -> Int = { s -> s.toIntOrNull() }
* ...
* ```
*
* In order to properly encode errors, we will ascribe an error type to any `null` values, such that
* parsing looks like this:
* ```
* val mapping: (String) -> T? = {...} // for some T
* val parser: (String) -> Result<T> = { s ->
* mapping(s)?.let {
* Result.success(it)
* } ?: Result.failure(/* some failure type */)
* }
* ```
*
* Composition
*
* The ability to compose value parsing enables us to provide a couple of reasonable default parsers
* and allow clients to seamlessly build upon that using map functions. Consider the case where we
* want to validate that a value is an [Int] between 0 and 100. We start with the generic [Int]
* parser, and a validator, of the type (Int) -> Result<Int>:
* ```
* val intParser = { s ->
* s.toStringOrNull().?let {...} ?: ...
* }
*
* val validator = { i ->
* if (i > 100 || i < 0) {
* Result.failure(...)
* } else {
* Result.success(i)
* }
* ```
*
* In order to combine these functions, we need to define a new [flatMap] function that can get us
* from a `Result<T>` to a `Result<R>`, and short-circuit on any error. We want to see this:
* ```
* val validatingParser = { s ->
* intParser.invoke(s).flatMap { i ->
* validator(i)
* }
* }
* ```
*
* The flatMap is relatively simply defined, we can mimic the existing definition for [Result.map],
* though the implementation is uglier because of the `internal` definition for `value`
*
* ```
* inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
* return when {
* isSuccess -> transform(getOrThrow())
* else -> Result.failure(exceptionOrNull()!!)
* }
* }
* ```
*/
/**
* Given a [transform] that returns a [Result], apply the transform to this result, unwrapping the
* return value so that
*
* These [contract] and [callsInPlace] methods are copied from the [Result.map] definition
*/
@OptIn(ExperimentalContracts::class)
inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) }
return when {
// Should never throw, we just don't have access to [this.value]
isSuccess -> transform(getOrThrow())
// Exception should never be null here
else -> Result.failure(exceptionOrNull()!!)
}
}
/**
* ValueParser turns a [String] into a Result<A> by applying a transform. See the default
* implementations below for starting points. The intention here is to provide the base mappings and
* allow clients to attach their own transforms. They are expected to succeed or return null on
* failure. The failure is propagated to the command parser as a Result and will fail on any
* [Result.failure]
*/
fun interface ValueParser<out A> {
fun parseValue(value: String): Result<A>
}
/** Map a [ValueParser] of type A to one of type B, by applying the given [transform] */
inline fun <A, B> ValueParser<A>.map(crossinline transform: (A) -> B?): ValueParser<B> {
return ValueParser<B> { value ->
this.parseValue(value).flatMap { a ->
transform(a)?.let { b -> Result.success(b) }
?: Result.failure(ArgParseError("Failed to transform value $value"))
}
}
}
/**
* Base type parsers are provided by the lib, and can be simply composed upon by [ValueParser.map]
* functions on the parser
*/
/** String parsing always succeeds if the value exists */
private val parseString: ValueParser<String> = ValueParser { value -> Result.success(value) }
private val parseBoolean: ValueParser<Boolean> = ValueParser { value ->
value.toBooleanStrictOrNull()?.let { Result.success(it) }
?: Result.failure(ArgParseError("Failed to parse $value as a boolean"))
}
private val parseInt: ValueParser<Int> = ValueParser { value ->
value.toIntOrNull()?.let { Result.success(it) }
?: Result.failure(ArgParseError("Failed to parse $value as an int"))
}
private val parseFloat: ValueParser<Float> = ValueParser { value ->
value.toFloatOrNull()?.let { Result.success(it) }
?: Result.failure(ArgParseError("Failed to parse $value as a float"))
}
/** Default parsers that can be use as-is, or [map]ped to another type */
object Type {
val Boolean = parseBoolean
val Int = parseInt
val Float = parseFloat
val String = parseString
}

View File

@@ -96,6 +96,7 @@ import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.concurrency.FakeThreadFactory;
@@ -139,6 +140,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
@Mock
private Display mDisplay;
@Mock
private CommandRegistry mCommandRegistry;
@Mock
private UserTracker mUserTracker;
@Mock
private PrivacyDotViewController mDotViewController;
@@ -231,8 +234,9 @@ public class ScreenDecorationsTest extends SysuiTestCase {
mExecutor,
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings,
mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController,
mThreadFactory,
mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
mAuthController) {
@@ -1226,8 +1230,9 @@ public class ScreenDecorationsTest extends SysuiTestCase {
mFaceScanningProviders.add(mFaceScanningDecorProvider);
when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
mSecureSettings, mUserTracker, mDisplayTracker, mDotViewController,
ScreenDecorations screenDecorations = new ScreenDecorations(mContext,
mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker,
mDotViewController,
mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
screenDecorations.start();

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
@SmallTest
class CommandParserTest : SysuiTestCase() {
private val parser = CommandParser()
@Test
fun registerToken_cannotReuseNames() {
parser.flag("-f")
assertThrows(IllegalArgumentException::class.java) { parser.flag("-f") }
}
@Test
fun unknownToken_throws() {
assertThrows(ArgParseError::class.java) { parser.parse(listOf("unknown-token")) }
}
@Test
fun parseSingleFlag_present() {
val flag by parser.flag("-f")
parser.parse(listOf("-f"))
assertTrue(flag)
}
@Test
fun parseSingleFlag_notPresent() {
val flag by parser.flag("-f")
parser.parse(listOf())
assertFalse(flag)
}
@Test
fun parseSingleOptionalParam_present() {
val param by parser.param("-p", valueParser = Type.Int)
parser.parse(listOf("-p", "123"))
assertThat(param).isEqualTo(123)
}
@Test
fun parseSingleOptionalParam_notPresent() {
val param by parser.param("-p", valueParser = Type.Int)
parser.parse(listOf())
assertThat(param).isNull()
}
@Test
fun parseSingleOptionalParam_missingArg_throws() {
val param by parser.param("-p", valueParser = Type.Int)
assertThrows(ArgParseError::class.java) { parser.parse(listOf("-p")) }
}
@Test
fun parseSingleRequiredParam_present() {
val param by parser.require(parser.param("-p", valueParser = Type.Int))
parser.parse(listOf("-p", "123"))
assertThat(param).isEqualTo(123)
}
@Test
fun parseSingleRequiredParam_notPresent_failsValidation() {
val param by parser.require(parser.param("-p", valueParser = Type.Int))
assertFalse(parser.parse(listOf()))
}
@Test
fun parseSingleRequiredParam_missingArg_throws() {
val param by parser.require(parser.param("-p", valueParser = Type.Int))
assertThrows(ArgParseError::class.java) { parser.parse(listOf("-p")) }
}
@Test
fun parseAsSubCommand_singleFlag_present() {
val flag by parser.flag("-f")
val args = listOf("-f").listIterator()
parser.parseAsSubCommand(args)
assertTrue(flag)
}
@Test
fun parseAsSubCommand_singleFlag_notPresent() {
val flag by parser.flag("-f")
val args = listOf("--other-flag").listIterator()
parser.parseAsSubCommand(args)
assertFalse(flag)
}
@Test
fun parseAsSubCommand_singleOptionalParam_present() {
val param by parser.param("-p", valueParser = Type.Int)
parser.parseAsSubCommand(listOf("-p", "123", "--other-arg", "321").listIterator())
assertThat(param).isEqualTo(123)
}
@Test
fun parseAsSubCommand_singleOptionalParam_notPresent() {
val param by parser.param("-p", valueParser = Type.Int)
parser.parseAsSubCommand(listOf("--other-arg", "321").listIterator())
assertThat(param).isNull()
}
@Test
fun parseAsSubCommand_singleRequiredParam_present() {
val param by parser.require(parser.param("-p", valueParser = Type.Int))
parser.parseAsSubCommand(listOf("-p", "123", "--other-arg", "321").listIterator())
assertThat(param).isEqualTo(123)
}
@Test
fun parseAsSubCommand_singleRequiredParam_notPresent() {
parser.require(parser.param("-p", valueParser = Type.Int))
assertFalse(parser.parseAsSubCommand(listOf("--other-arg", "321").listIterator()))
}
@Test
fun parseCommandWithSubCommand_required_provided() {
val topLevelFlag by parser.flag("flag", shortName = "-f")
val cmd =
object : ParseableCommand("test") {
val flag by flag("flag1")
override fun execute(pw: PrintWriter) {}
}
parser.require(parser.subCommand(cmd))
parser.parse(listOf("-f", "test", "--flag1"))
assertTrue(topLevelFlag)
assertThat(cmd).isNotNull()
assertTrue(cmd.flag)
}
@Test
fun parseCommandWithSubCommand_required_notProvided() {
val topLevelFlag by parser.flag("-f")
val cmd =
object : ParseableCommand("test") {
val flag by parser.flag("flag1")
override fun execute(pw: PrintWriter) {}
}
parser.require(parser.subCommand(cmd))
assertFalse(parser.parse(listOf("-f")))
}
@Test
fun flag_requiredParam_optionalParam_allProvided_failsValidation() {
val flag by parser.flag("-f")
val optionalParam by parser.param("-p", valueParser = Type.Int)
val requiredParam by parser.require(parser.param("-p2", valueParser = Type.Boolean))
parser.parse(
listOf(
"-f",
"-p",
"123",
"-p2",
"false",
)
)
assertTrue(flag)
assertThat(optionalParam).isEqualTo(123)
assertFalse(requiredParam)
}
@Test
fun flag_requiredParam_optionalParam_optionalExcluded() {
val flag by parser.flag("-f")
val optionalParam by parser.param("-p", valueParser = Type.Int)
val requiredParam by parser.require(parser.param("-p2", valueParser = Type.Boolean))
parser.parse(
listOf(
"-p2",
"true",
)
)
assertFalse(flag)
assertThat(optionalParam).isNull()
assertTrue(requiredParam)
}
}

View File

@@ -0,0 +1,55 @@
package com.android.systemui.statusbar.commandline
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
@SmallTest
class ParametersTest : SysuiTestCase() {
@Test
fun singleArgOptional_returnsNullBeforeParse() {
val optional by SingleArgParamOptional(longName = "longName", valueParser = Type.Int)
assertThat(optional).isNull()
}
@Test
fun singleArgOptional_returnsParsedValue() {
val param = SingleArgParamOptional(longName = "longName", valueParser = Type.Int)
param.parseArgsFromIter(listOf("3").listIterator())
val optional by param
assertThat(optional).isEqualTo(3)
}
@Test
fun singleArgRequired_throwsBeforeParse() {
val req by SingleArgParam(longName = "param", valueParser = Type.Boolean)
assertThrows(IllegalStateException::class.java) { req }
}
@Test
fun singleArgRequired_returnsParsedValue() {
val param = SingleArgParam(longName = "param", valueParser = Type.Boolean)
param.parseArgsFromIter(listOf("true").listIterator())
val req by param
assertTrue(req)
}
@Test
fun param_handledAfterParse() {
val optParam = SingleArgParamOptional(longName = "string1", valueParser = Type.String)
val reqParam = SingleArgParam(longName = "string2", valueParser = Type.Float)
assertFalse(optParam.handled)
assertFalse(reqParam.handled)
optParam.parseArgsFromIter(listOf("test").listIterator())
reqParam.parseArgsFromIter(listOf("1.23").listIterator())
assertTrue(optParam.handled)
assertTrue(reqParam.handled)
}
}

View File

@@ -0,0 +1,317 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.commandline
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
class ParseableCommandTest : SysuiTestCase() {
@Mock private lateinit var pw: PrintWriter
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
/**
* A little change-detector-y, but this is just a general assertion that building up a command
* parser via its wrapper works as expected.
*/
@Test
fun testFactoryMethods() {
val mySubCommand =
object : ParseableCommand("subCommand") {
val flag by flag("flag")
override fun execute(pw: PrintWriter) {}
}
val mySubCommand2 =
object : ParseableCommand("subCommand2") {
val flag by flag("flag")
override fun execute(pw: PrintWriter) {}
}
// Verify that the underlying parser contains the correct types
val myCommand =
object : ParseableCommand("testName") {
val flag by flag("flag", shortName = "f")
val requiredParam by
param(longName = "required-param", shortName = "r", valueParser = Type.String)
.required()
val optionalParam by
param(longName = "optional-param", shortName = "o", valueParser = Type.Boolean)
val optionalSubCommand by subCommand(mySubCommand)
val requiredSubCommand by subCommand(mySubCommand2).required()
override fun execute(pw: PrintWriter) {}
}
val flags = myCommand.parser.flags
val params = myCommand.parser.params
val subCommands = myCommand.parser.subCommands
assertThat(flags).hasSize(2)
assertThat(flags[0]).isInstanceOf(Flag::class.java)
assertThat(flags[1]).isInstanceOf(Flag::class.java)
assertThat(params).hasSize(2)
val req = params.filter { it is SingleArgParam<*> }
val opt = params.filter { it is SingleArgParamOptional<*> }
assertThat(req).hasSize(1)
assertThat(opt).hasSize(1)
val reqSub = subCommands.filter { it is RequiredSubCommand<*> }
val optSub = subCommands.filter { it is OptionalSubCommand<*> }
assertThat(reqSub).hasSize(1)
assertThat(optSub).hasSize(1)
}
@Test
fun factoryMethods_enforceShortNameRules() {
// Short names MUST be one character long
assertThrows(IllegalArgumentException::class.java) {
val myCommand =
object : ParseableCommand("test-command") {
val flag by flag("longName", "invalidShortName")
override fun execute(pw: PrintWriter) {}
}
}
assertThrows(IllegalArgumentException::class.java) {
val myCommand =
object : ParseableCommand("test-command") {
val param by param("longName", "invalidShortName", valueParser = Type.String)
override fun execute(pw: PrintWriter) {}
}
}
}
@Test
fun factoryMethods_enforceLongNames_notPrefixed() {
// Long names must not start with "-", since they will be added
assertThrows(IllegalArgumentException::class.java) {
val myCommand =
object : ParseableCommand("test-command") {
val flag by flag("--invalid")
override fun execute(pw: PrintWriter) {}
}
}
assertThrows(IllegalArgumentException::class.java) {
val myCommand =
object : ParseableCommand("test-command") {
val param by param("-invalid", valueParser = Type.String)
override fun execute(pw: PrintWriter) {}
}
}
}
@Test
fun executeDoesNotPropagateExceptions() {
val cmd =
object : ParseableCommand("test-command") {
val flag by flag("flag")
override fun execute(pw: PrintWriter) {}
}
val throwingCommand = listOf("unknown-token")
// Given a command that would cause an ArgParseError
assertThrows(ArgParseError::class.java) { cmd.parser.parse(throwingCommand) }
// The parser consumes that error
cmd.execute(pw, throwingCommand)
}
@Test
fun executeFailingCommand_callsOnParseFailed() {
val cmd =
object : ParseableCommand("test-command") {
val flag by flag("flag")
var onParseFailedCalled = false
override fun execute(pw: PrintWriter) {}
override fun onParseFailed(error: ArgParseError) {
onParseFailedCalled = true
}
}
val throwingCommand = listOf("unknown-token")
cmd.execute(pw, throwingCommand)
assertTrue(cmd.onParseFailedCalled)
}
@Test
fun baseCommand() {
val myCommand = MyCommand()
myCommand.execute(pw, baseCommand)
assertThat(myCommand.flag1).isFalse()
assertThat(myCommand.singleParam).isNull()
}
@Test
fun commandWithFlags() {
val command = MyCommand()
command.execute(pw, cmdWithFlags)
assertThat(command.flag1).isTrue()
assertThat(command.flag2).isTrue()
}
@Test
fun commandWithArgs() {
val cmd = MyCommand()
cmd.execute(pw, cmdWithSingleArgParam)
assertThat(cmd.singleParam).isEqualTo("single_param")
}
@Test
fun commandWithRequiredParam_provided() {
val cmd =
object : ParseableCommand(name) {
val singleRequiredParam: String by
param(
longName = "param1",
shortName = "p",
valueParser = Type.String,
)
.required()
override fun execute(pw: PrintWriter) {}
}
val cli = listOf("-p", "value")
cmd.execute(pw, cli)
assertThat(cmd.singleRequiredParam).isEqualTo("value")
}
@Test
fun commandWithRequiredParam_not_provided_throws() {
val cmd =
object : ParseableCommand(name) {
val singleRequiredParam by
param(shortName = "p", longName = "param1", valueParser = Type.String)
.required()
override fun execute(pw: PrintWriter) {}
override fun execute(pw: PrintWriter, args: List<String>) {
parser.parse(args)
execute(pw)
}
}
val cli = listOf("")
assertThrows(ArgParseError::class.java) { cmd.execute(pw, cli) }
}
@Test
fun commandWithSubCommand() {
val subName = "sub-command"
val subCmd =
object : ParseableCommand(subName) {
val singleOptionalParam: String? by param("param", valueParser = Type.String)
override fun execute(pw: PrintWriter) {}
}
val cmd =
object : ParseableCommand(name) {
val subCmd by subCommand(subCmd)
override fun execute(pw: PrintWriter) {}
}
cmd.execute(pw, listOf("sub-command", "--param", "test"))
assertThat(cmd.subCmd?.singleOptionalParam).isEqualTo("test")
}
@Test
fun complexCommandWithSubCommands_reusedNames() {
val commandLine = "-f --param1 arg1 sub-command1 -f -p arg2 --param2 arg3".split(" ")
val subName = "sub-command1"
val subCmd =
object : ParseableCommand(subName) {
val flag1 by flag("flag", shortName = "f")
val param1: String? by param("param1", shortName = "p", valueParser = Type.String)
override fun execute(pw: PrintWriter) {}
}
val myCommand =
object : ParseableCommand(name) {
val flag1 by flag(longName = "flag", shortName = "f")
val param1 by param("param1", shortName = "p", valueParser = Type.String).required()
val param2: String? by param(longName = "param2", valueParser = Type.String)
val subCommand by subCommand(subCmd)
override fun execute(pw: PrintWriter) {}
}
myCommand.execute(pw, commandLine)
assertThat(myCommand.flag1).isTrue()
assertThat(myCommand.param1).isEqualTo("arg1")
assertThat(myCommand.param2).isEqualTo("arg3")
assertThat(myCommand.subCommand).isNotNull()
assertThat(myCommand.subCommand?.flag1).isTrue()
assertThat(myCommand.subCommand?.param1).isEqualTo("arg2")
}
class MyCommand(
private val onExecute: ((MyCommand) -> Unit)? = null,
) : ParseableCommand(name) {
val flag1 by flag(shortName = "f", longName = "flag1", description = "flag 1 for test")
val flag2 by flag(shortName = "g", longName = "flag2", description = "flag 2 for test")
val singleParam: String? by
param(
shortName = "a",
longName = "arg1",
valueParser = Type.String,
)
override fun execute(pw: PrintWriter) {
onExecute?.invoke(this)
}
}
companion object {
const val name = "my_command"
val baseCommand = listOf("")
val cmdWithFlags = listOf("-f", "--flag2")
val cmdWithSingleArgParam = listOf("--arg1", "single_param")
}
}

View File

@@ -0,0 +1,61 @@
package com.android.systemui.statusbar.commandline
import android.graphics.Rect
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertTrue
import org.junit.Test
@SmallTest
class ValueParserTest : SysuiTestCase() {
@Test
fun parseString() {
assertThat(Type.String.parseValue("test")).isEqualTo(Result.success("test"))
}
@Test
fun parseInt() {
assertThat(Type.Int.parseValue("123")).isEqualTo(Result.success(123))
assertTrue(Type.Int.parseValue("not an Int").isFailure)
}
@Test
fun parseFloat() {
assertThat(Type.Float.parseValue("1.23")).isEqualTo(Result.success(1.23f))
assertTrue(Type.Int.parseValue("not a Float").isFailure)
}
@Test
fun parseBoolean() {
assertThat(Type.Boolean.parseValue("true")).isEqualTo(Result.success(true))
assertThat(Type.Boolean.parseValue("false")).isEqualTo(Result.success(false))
assertTrue(Type.Boolean.parseValue("not a Boolean").isFailure)
}
@Test
fun mapToComplexType() {
val parseSquare = Type.Int.map { Rect(it, it, it, it) }
assertThat(parseSquare.parseValue("10")).isEqualTo(Result.success(Rect(10, 10, 10, 10)))
}
@Test
fun mapToFallibleComplexType() {
val fallibleParseSquare =
Type.Int.map {
if (it > 0) {
Rect(it, it, it, it)
} else {
null
}
}
assertThat(fallibleParseSquare.parseValue("10"))
.isEqualTo(Result.success(Rect(10, 10, 10, 10)))
assertTrue(fallibleParseSquare.parseValue("-10").isFailure)
}
}