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:
@@ -56,9 +56,14 @@ class ScreenDecorHwcLayer(
|
|||||||
) : DisplayCutoutBaseView(context) {
|
) : DisplayCutoutBaseView(context) {
|
||||||
val colorMode: Int
|
val colorMode: Int
|
||||||
private val useInvertedAlphaColor: Boolean
|
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 bgColor: Int
|
||||||
private val cornerFilter: ColorFilter
|
private var cornerFilter: ColorFilter
|
||||||
private val cornerBgFilter: ColorFilter
|
private val cornerBgFilter: ColorFilter
|
||||||
private val clearPaint: Paint
|
private val clearPaint: Paint
|
||||||
@JvmField val transparentRect: Rect = Rect()
|
@JvmField val transparentRect: Rect = Rect()
|
||||||
@@ -109,10 +114,16 @@ class ScreenDecorHwcLayer(
|
|||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
parent.requestTransparentRegion(this)
|
parent.requestTransparentRegion(this)
|
||||||
|
updateColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateColors() {
|
||||||
if (!debug) {
|
if (!debug) {
|
||||||
viewRootImpl.setDisplayDecoration(true)
|
viewRootImpl.setDisplayDecoration(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||||
|
|
||||||
if (useInvertedAlphaColor) {
|
if (useInvertedAlphaColor) {
|
||||||
paint.set(clearPaint)
|
paint.set(clearPaint)
|
||||||
} else {
|
} 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() {
|
override fun onUpdate() {
|
||||||
parent.requestTransparentRegion(this)
|
parent.requestTransparentRegion(this)
|
||||||
}
|
}
|
||||||
@@ -367,7 +393,7 @@ class ScreenDecorHwcLayer(
|
|||||||
/**
|
/**
|
||||||
* Update the rounded corner drawables.
|
* Update the rounded corner drawables.
|
||||||
*/
|
*/
|
||||||
fun updateRoundedCornerDrawable(top: Drawable, bottom: Drawable) {
|
fun updateRoundedCornerDrawable(top: Drawable?, bottom: Drawable?) {
|
||||||
roundedCornerDrawableTop = top
|
roundedCornerDrawableTop = top
|
||||||
roundedCornerDrawableBottom = bottom
|
roundedCornerDrawableBottom = bottom
|
||||||
updateRoundedCornerDrawableBounds()
|
updateRoundedCornerDrawableBounds()
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ import com.android.internal.util.Preconditions;
|
|||||||
import com.android.settingslib.Utils;
|
import com.android.settingslib.Utils;
|
||||||
import com.android.systemui.biometrics.AuthController;
|
import com.android.systemui.biometrics.AuthController;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
|
||||||
import com.android.systemui.decor.CutoutDecorProviderFactory;
|
import com.android.systemui.decor.CutoutDecorProviderFactory;
|
||||||
import com.android.systemui.decor.DebugRoundedCornerDelegate;
|
import com.android.systemui.decor.DebugRoundedCornerDelegate;
|
||||||
|
import com.android.systemui.decor.DebugRoundedCornerModel;
|
||||||
import com.android.systemui.decor.DecorProvider;
|
import com.android.systemui.decor.DecorProvider;
|
||||||
import com.android.systemui.decor.DecorProviderFactory;
|
import com.android.systemui.decor.DecorProviderFactory;
|
||||||
import com.android.systemui.decor.DecorProviderKt;
|
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.PrivacyDotDecorProviderFactory;
|
||||||
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
|
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
|
||||||
import com.android.systemui.decor.RoundedCornerResDelegateImpl;
|
import com.android.systemui.decor.RoundedCornerResDelegateImpl;
|
||||||
|
import com.android.systemui.decor.ScreenDecorCommand;
|
||||||
import com.android.systemui.log.ScreenDecorationsLogger;
|
import com.android.systemui.log.ScreenDecorationsLogger;
|
||||||
import com.android.systemui.qs.SettingObserver;
|
import com.android.systemui.qs.SettingObserver;
|
||||||
import com.android.systemui.settings.DisplayTracker;
|
import com.android.systemui.settings.DisplayTracker;
|
||||||
import com.android.systemui.settings.UserTracker;
|
import com.android.systemui.settings.UserTracker;
|
||||||
|
import com.android.systemui.statusbar.commandline.CommandRegistry;
|
||||||
import com.android.systemui.statusbar.events.PrivacyDotViewController;
|
import com.android.systemui.statusbar.events.PrivacyDotViewController;
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
import com.android.systemui.util.concurrency.ThreadFactory;
|
import com.android.systemui.util.concurrency.ThreadFactory;
|
||||||
@@ -95,7 +97,6 @@ import java.io.PrintWriter;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
protected boolean mIsRegistered;
|
protected boolean mIsRegistered;
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final Executor mMainExecutor;
|
private final CommandRegistry mCommandRegistry;
|
||||||
private final SecureSettings mSecureSettings;
|
private final SecureSettings mSecureSettings;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
DisplayTracker.Callback mDisplayListener;
|
DisplayTracker.Callback mDisplayListener;
|
||||||
@@ -313,8 +314,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ScreenDecorations(Context context,
|
public ScreenDecorations(Context context,
|
||||||
@Main Executor mainExecutor,
|
|
||||||
SecureSettings secureSettings,
|
SecureSettings secureSettings,
|
||||||
|
CommandRegistry commandRegistry,
|
||||||
UserTracker userTracker,
|
UserTracker userTracker,
|
||||||
DisplayTracker displayTracker,
|
DisplayTracker displayTracker,
|
||||||
PrivacyDotViewController dotViewController,
|
PrivacyDotViewController dotViewController,
|
||||||
@@ -324,8 +325,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
|
|||||||
ScreenDecorationsLogger logger,
|
ScreenDecorationsLogger logger,
|
||||||
AuthController authController) {
|
AuthController authController) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mMainExecutor = mainExecutor;
|
|
||||||
mSecureSettings = secureSettings;
|
mSecureSettings = secureSettings;
|
||||||
|
mCommandRegistry = commandRegistry;
|
||||||
mUserTracker = userTracker;
|
mUserTracker = userTracker;
|
||||||
mDisplayTracker = displayTracker;
|
mDisplayTracker = displayTracker;
|
||||||
mDotViewController = dotViewController;
|
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
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
|
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
|
||||||
@@ -361,6 +401,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
|
|||||||
mExecutor.execute(this::startOnScreenDecorationsThread);
|
mExecutor.execute(this::startOnScreenDecorationsThread);
|
||||||
mDotViewController.setUiExecutor(mExecutor);
|
mDotViewController.setUiExecutor(mExecutor);
|
||||||
mAuthController.addCallback(mAuthControllerCallback);
|
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();
|
bottomDrawable = mDebugRoundedCornerDelegate.getBottomRoundedDrawable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topDrawable == null || bottomDrawable == null) {
|
if (topDrawable == null && bottomDrawable == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);
|
mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);
|
||||||
|
|||||||
@@ -77,16 +77,30 @@ class DebugRoundedCornerDelegate : RoundedCornerResDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun applyNewDebugCorners(
|
fun applyNewDebugCorners(
|
||||||
topCorner: DebugRoundedCornerModel,
|
topCorner: DebugRoundedCornerModel?,
|
||||||
bottomCorner: DebugRoundedCornerModel,
|
bottomCorner: DebugRoundedCornerModel?,
|
||||||
) {
|
) {
|
||||||
hasTop = true
|
topCorner?.let {
|
||||||
topRoundedDrawable = topCorner.toPathDrawable(paint)
|
hasTop = true
|
||||||
topRoundedSize = topCorner.size()
|
topRoundedDrawable = it.toPathDrawable(paint)
|
||||||
|
topRoundedSize = it.size()
|
||||||
|
}
|
||||||
|
?: {
|
||||||
|
hasTop = false
|
||||||
|
topRoundedDrawable = null
|
||||||
|
topRoundedSize = Size(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
hasBottom = true
|
bottomCorner?.let {
|
||||||
bottomRoundedDrawable = bottomCorner.toPathDrawable(paint)
|
hasBottom = true
|
||||||
bottomRoundedSize = bottomCorner.size()
|
bottomRoundedDrawable = it.toPathDrawable(paint)
|
||||||
|
bottomRoundedSize = it.size()
|
||||||
|
}
|
||||||
|
?: {
|
||||||
|
hasBottom = false
|
||||||
|
bottomRoundedDrawable = null
|
||||||
|
bottomRoundedSize = Size(0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -96,6 +96,7 @@ import com.android.systemui.log.ScreenDecorationsLogger;
|
|||||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.settings.FakeDisplayTracker;
|
import com.android.systemui.settings.FakeDisplayTracker;
|
||||||
import com.android.systemui.settings.UserTracker;
|
import com.android.systemui.settings.UserTracker;
|
||||||
|
import com.android.systemui.statusbar.commandline.CommandRegistry;
|
||||||
import com.android.systemui.statusbar.events.PrivacyDotViewController;
|
import com.android.systemui.statusbar.events.PrivacyDotViewController;
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor;
|
import com.android.systemui.util.concurrency.FakeExecutor;
|
||||||
import com.android.systemui.util.concurrency.FakeThreadFactory;
|
import com.android.systemui.util.concurrency.FakeThreadFactory;
|
||||||
@@ -139,6 +140,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
|||||||
@Mock
|
@Mock
|
||||||
private Display mDisplay;
|
private Display mDisplay;
|
||||||
@Mock
|
@Mock
|
||||||
|
private CommandRegistry mCommandRegistry;
|
||||||
|
@Mock
|
||||||
private UserTracker mUserTracker;
|
private UserTracker mUserTracker;
|
||||||
@Mock
|
@Mock
|
||||||
private PrivacyDotViewController mDotViewController;
|
private PrivacyDotViewController mDotViewController;
|
||||||
@@ -231,8 +234,9 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
|||||||
mExecutor,
|
mExecutor,
|
||||||
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
|
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
|
||||||
|
|
||||||
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
|
mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings,
|
||||||
mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
|
mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController,
|
||||||
|
mThreadFactory,
|
||||||
mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
|
mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
|
||||||
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
|
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
|
||||||
mAuthController) {
|
mAuthController) {
|
||||||
@@ -1226,8 +1230,9 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
|||||||
mFaceScanningProviders.add(mFaceScanningDecorProvider);
|
mFaceScanningProviders.add(mFaceScanningDecorProvider);
|
||||||
when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
|
when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
|
||||||
when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
|
when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
|
||||||
ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
|
ScreenDecorations screenDecorations = new ScreenDecorations(mContext,
|
||||||
mSecureSettings, mUserTracker, mDisplayTracker, mDotViewController,
|
mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker,
|
||||||
|
mDotViewController,
|
||||||
mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
|
mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
|
||||||
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
|
new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
|
||||||
screenDecorations.start();
|
screenDecorations.start();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user