Adds a CLI (Command-Line Interface) to KitchenSink.
Examples:
$ alias ks_cmd='adb shell dumpsys activity com.google.android.car.kitchensink/.KitchenSinkActivity cmd'
$ ks_cmd help
TASK 1010085:com.google.android.car.kitchensink id=1000039 userId=10
ACTIVITY com.google.android.car.kitchensink/.KitchenSinkActivity d39dc7c pid=10541
KitchenSink Command-Line Interface. Available commands:
help:
Shows this help message.
get-delegated-scopes:
Lists delegated scopes set by the device admin.
is-uninstall-blocked <PKG>:
Checks whether uninstalling the given app is blocked.
set-uninstall-blocked <PKG> <true|false>:
Blocks / unblocks uninstalling the given app.
generate-device-attestation-key-pair <ALIAS> [FLAGS]:
Generates a device attestation key.
$ ks_cmd get-delegated-scopes
TASK 1010085:com.google.android.car.kitchensink id=1000039 userId=10
ACTIVITY com.google.android.car.kitchensink/.KitchenSinkActivity d39dc7c pid=10541
3 delegated scopes:
delegation-block-uninstall
delegation-package-access
delegation-cert-install
Test: see above
Bug: 213388897
Change-Id: I58623f5c1a1d60af7743f5a815a491ebbad9b9fe
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index fd8819a..5574465 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -85,6 +85,8 @@
import com.google.android.car.kitchensink.watchdog.CarWatchdogTestFragment;
import com.google.android.car.kitchensink.weblinks.WebLinksTestFragment;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -401,6 +403,17 @@
super.onDestroy();
}
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (args != null && args.length > 0 && args[0].equals("cmd")) {
+ String[] cmdArgs = new String[args.length - 1];
+ System.arraycopy(args, 1, cmdArgs, 0, args.length - 1);
+ new KitchenSinkShellCommand(this, writer, cmdArgs).run();
+ return;
+ }
+ super.dump(prefix, fd, writer, args);
+ }
+
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.kitchen_content, fragment)
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkShellCommand.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkShellCommand.java
new file mode 100644
index 0000000..d72765e
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkShellCommand.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2022 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.google.android.car.kitchensink;
+
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.security.AttestedKeyPair;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * {@code KitchenSink}'s own {@code cmd} implementation.
+ *
+ * <p>Usage: {$code adb shell dumpsys activity
+ * com.google.android.car.kitchensink/.KitchenSinkActivity cmd <CMD>}
+ *
+ * <p><p>Note</p>: you must launch {@code KitchenSink} first. Example: {@code
+ * adb shell am start com.google.android.car.kitchensink/.KitchenSinkActivity}
+ */
+final class KitchenSinkShellCommand {
+
+ private static final String TAG = "KitchenSinkCmd";
+
+ private static final String CMD_HELP = "help";
+ private static final String CMD_GET_DELEGATED_SCOPES = "get-delegated-scopes";
+ private static final String CMD_IS_UNINSTALL_BLOCKED = "is-uninstall-blocked";
+ private static final String CMD_SET_UNINSTALL_BLOCKED = "set-uninstall-blocked";
+ private static final String CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR =
+ "generate-device-attestation-key-pair";
+
+ private final Context mContext;
+ private final @Nullable DevicePolicyManager mDpm;
+ private final IndentingPrintWriter mWriter;
+ private final String[] mArgs;
+
+ @Nullable // dynamically created on post() method
+ private Handler mHandler;
+
+ private int mNextArgIndex;
+
+ KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args) {
+ mContext = context;
+ mDpm = context.getSystemService(DevicePolicyManager.class);
+ mWriter = new IndentingPrintWriter(writer);
+ mArgs = args;
+ }
+
+ void run() {
+ if (mArgs.length == 0) {
+ showHelp("Error: must pass an argument");
+ return;
+ }
+ String cmd = mArgs[0];
+ switch (cmd) {
+ case CMD_HELP:
+ showHelp("KitchenSink Command-Line Interface");
+ break;
+ case CMD_GET_DELEGATED_SCOPES:
+ getDelegatedScopes();
+ break;
+ case CMD_IS_UNINSTALL_BLOCKED:
+ isUninstallBlocked();
+ break;
+ case CMD_SET_UNINSTALL_BLOCKED:
+ setUninstallBlocked();
+ break;
+ case CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR:
+ generateDeviceAttestationKeyPair();
+ break;
+ default:
+ showHelp("Invalid command: %s", cmd);
+ }
+ }
+
+ private void showHelp(String headerMessage, Object... headerArgs) {
+ if (headerMessage != null) {
+ mWriter.printf(headerMessage, headerArgs);
+ mWriter.print(". ");
+ }
+ mWriter.println("Available commands:\n");
+
+ mWriter.increaseIndent();
+ showCommandHelp("Shows this help message.",
+ CMD_HELP);
+ showCommandHelp("Lists delegated scopes set by the device admin.",
+ CMD_GET_DELEGATED_SCOPES);
+ showCommandHelp("Checks whether uninstalling the given app is blocked.",
+ CMD_IS_UNINSTALL_BLOCKED, "<PKG>");
+ showCommandHelp("Blocks / unblocks uninstalling the given app.",
+ CMD_SET_UNINSTALL_BLOCKED, "<PKG>", "<true|false>");
+ showCommandHelp("Generates a device attestation key.",
+ CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR, "<ALIAS>", "[FLAGS]");
+ mWriter.decreaseIndent();
+ }
+
+ private void showCommandHelp(String description, String cmd, String... args) {
+ mWriter.printf("%s", cmd);
+ if (args != null) {
+ for (String arg : args) {
+ mWriter.printf(" %s", arg);
+ }
+ }
+ mWriter.println(":");
+ mWriter.increaseIndent();
+ mWriter.printf("%s\n\n", description);
+ mWriter.decreaseIndent();
+ }
+
+ private void getDelegatedScopes() {
+ if (!supportDevicePolicyManagement()) return;
+
+ List<String> scopes = mDpm.getDelegatedScopes(/* admin= */ null, mContext.getPackageName());
+ printCollection("delegated scope", scopes);
+ }
+
+ private void isUninstallBlocked() {
+ if (!supportDevicePolicyManagement()) return;
+
+ String packageName = getNextArg();
+ boolean isIt = mDpm.isUninstallBlocked(/* admin= */ null, packageName);
+ mWriter.println(isIt);
+ }
+
+ private void setUninstallBlocked() {
+ if (!supportDevicePolicyManagement()) return;
+
+ String packageName = getNextArg();
+ boolean blocked = getNextBooleanArg();
+
+ Log.i(TAG, "Calling dpm.setUninstallBlocked(" + packageName + ", " + blocked + ")");
+ mDpm.setUninstallBlocked(/* admin= */ null, packageName, blocked);
+ }
+
+ private void generateDeviceAttestationKeyPair() {
+ if (!supportDevicePolicyManagement()) return;
+
+ String alias = getNextArg();
+ int flags = getNextOptionalIntArg(/* defaultValue= */ 0);
+ // Cannot call dpm.generateKeyPair() on main thread
+ warnAboutAsyncCall();
+ post(() -> handleDeviceAttestationKeyPair(alias, flags));
+ }
+
+ private void handleDeviceAttestationKeyPair(String alias, int flags) {
+ KeyGenParameterSpec keySpec = buildRsaKeySpecWithKeyAttestation(alias);
+ String algorithm = "RSA";
+ Log.i(TAG, "calling dpm.generateKeyPair(alg=" + algorithm + ", spec=" + keySpec
+ + ", flags=" + flags + ")");
+ AttestedKeyPair kp = mDpm.generateKeyPair(/* admin= */ null, algorithm, keySpec, flags);
+ Log.i(TAG, "key: " + kp);
+ }
+
+ private void warnAboutAsyncCall() {
+ mWriter.printf("Command will be executed asynchronally; use `adb logcat %s *:s` for result"
+ + "\n", TAG);
+ }
+
+ private void post(Runnable r) {
+ if (mHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("KitchenSinkShellCommandThread");
+ Log.i(TAG, "Starting " + handlerThread);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ }
+ Log.d(TAG, "posting runnable");
+ mHandler.post(r);
+ }
+
+ private boolean supportDevicePolicyManagement() {
+ if (mDpm == null) {
+ mWriter.println("Device Policy Management not supported by device");
+ return false;
+ }
+ return true;
+ }
+
+ private String getNextArgAndIncrementCounter() {
+ return mArgs[++mNextArgIndex];
+ }
+
+ private String getNextArg() {
+ try {
+ return getNextArgAndIncrementCounter();
+ } catch (Exception e) {
+ mWriter.println("Error: missing argument");
+ mWriter.flush();
+ throw new IllegalArgumentException(
+ "Missing argument. Args=" + Arrays.toString(mArgs));
+ }
+ }
+
+ private int getNextOptionalIntArg(int defaultValue) {
+ try {
+ return Integer.parseInt(getNextArgAndIncrementCounter());
+ } catch (Exception e) {
+ Log.d(TAG, "Exception getting optional arg: " + e);
+ return defaultValue;
+ }
+ }
+
+ private boolean getNextBooleanArg() {
+ String arg = getNextArg();
+ return Boolean.parseBoolean(arg);
+ }
+
+ private void printCollection(String nameOnSingular, Collection<String> collection) {
+ if (collection.isEmpty()) {
+ mWriter.printf("No %ss\n", nameOnSingular);
+ return;
+ }
+ int size = collection.size();
+ mWriter.printf("%d %s%s:\n", size, nameOnSingular, size == 1 ? "" : "s");
+ collection.forEach((s) -> mWriter.printf(" %s\n", s));
+ }
+
+ // Copied from CTS' KeyGenerationUtils
+ private static KeyGenParameterSpec buildRsaKeySpecWithKeyAttestation(String alias) {
+ return new KeyGenParameterSpec.Builder(alias,
+ KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+ .setKeySize(2048)
+ .setDigests(KeyProperties.DIGEST_SHA256)
+ .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS,
+ KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+ .setIsStrongBoxBacked(false)
+ .setAttestationChallenge(new byte[] {
+ 'a', 'b', 'c'
+ })
+ .build();
+ }
+}