1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.car.kitchensink; 17 18 import android.annotation.Nullable; 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.admin.DevicePolicyManager; 23 import android.content.Context; 24 import android.os.Handler; 25 import android.os.HandlerThread; 26 import android.os.UserManager; 27 import android.security.AttestedKeyPair; 28 import android.security.keystore.KeyGenParameterSpec; 29 import android.security.keystore.KeyProperties; 30 import android.util.IndentingPrintWriter; 31 import android.util.Log; 32 import android.widget.Toast; 33 34 import com.google.android.car.kitchensink.drivemode.DriveModeSwitchController; 35 36 import java.io.FileDescriptor; 37 import java.io.PrintWriter; 38 import java.util.Arrays; 39 import java.util.Collection; 40 import java.util.List; 41 42 /** 43 * {@code KitchenSink}'s own {@code cmd} implementation. 44 * 45 * <p>Usage: {$code adb shell dumpsys activity 46 * com.google.android.car.kitchensink/.KitchenSinkActivity cmd <CMD>} 47 * 48 * <p><p>Note</p>: this class is meant only for "global" commands (i.e., actions that could be 49 * applied regardless of the current {@code KitchenSink} fragment), or for commands that don't have 50 * an equivalent UI (for example, the key attestation ones). If you want to provide commands to 51 * control the behavior of a fragment, you should implement {@code dump} on that fragment directly 52 * (see 53 * {@link com.google.android.car.kitchensink.VirtualDisplayFragment#dump(String,FileDescriptor,PrintWriter,String[])} 54 * as an example); 55 * 56 * <p><p>Note</p>: you must launch {@code KitchenSink} first. Example: {@code 57 * adb shell am start com.google.android.car.kitchensink/.KitchenSinkActivity} 58 */ 59 final class KitchenSinkShellCommand { 60 61 private static final String TAG = "KitchenSinkCmd"; 62 63 private static final String CMD_HELP = "help"; 64 private static final String CMD_GET_DELEGATED_SCOPES = "get-delegated-scopes"; 65 private static final String CMD_IS_UNINSTALL_BLOCKED = "is-uninstall-blocked"; 66 private static final String CMD_SET_UNINSTALL_BLOCKED = "set-uninstall-blocked"; 67 private static final String CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR = 68 "generate-device-attestation-key-pair"; 69 private static final String CMD_POST_NOTIFICATION = "post-notification"; 70 private static final String CMD_POST_TOAST = "post-toast"; 71 private static final String CMD_SET_DRIVE_MODE_SWITCH= "set-drive-mode-switch"; 72 73 private static final String ARG_VERBOSE = "-v"; 74 private static final String ARG_VERBOSE_FULL = "--verbose"; 75 private static final String ARG_USES_APP_CONTEXT = "--app-context"; 76 private static final String ARG_LONG_TOAST = "--long-toast"; 77 78 private final Context mContext; 79 private final @Nullable DevicePolicyManager mDpm; 80 private final IndentingPrintWriter mWriter; 81 private final String[] mArgs; 82 private final int mNotificationId; 83 84 @Nullable // dynamically created on post() method 85 private Handler mHandler; 86 87 private int mNextArgIndex; 88 KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args, int id)89 KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args, int id) { 90 mContext = context; 91 mDpm = context.getSystemService(DevicePolicyManager.class); 92 mWriter = new IndentingPrintWriter(writer); 93 mArgs = args; 94 mNotificationId = id; 95 } 96 run()97 void run() { 98 if (mArgs.length == 0) { 99 showHelp("Error: must pass an argument"); 100 return; 101 } 102 String cmd = mArgs[0]; 103 switch (cmd) { 104 case CMD_HELP: 105 showHelp("KitchenSink Command-Line Interface"); 106 break; 107 case CMD_GET_DELEGATED_SCOPES: 108 getDelegatedScopes(); 109 break; 110 case CMD_IS_UNINSTALL_BLOCKED: 111 isUninstallBlocked(); 112 break; 113 case CMD_SET_UNINSTALL_BLOCKED: 114 setUninstallBlocked(); 115 break; 116 case CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR: 117 generateDeviceAttestationKeyPair(); 118 break; 119 case CMD_POST_NOTIFICATION: 120 postNotification(); 121 break; 122 case CMD_POST_TOAST: 123 postToast(); 124 break; 125 case CMD_SET_DRIVE_MODE_SWITCH: 126 setDriveModeSwitch(); 127 break; 128 default: 129 showHelp("Invalid command: %s", cmd); 130 } 131 } 132 showHelp(String headerMessage, Object... headerArgs)133 private void showHelp(String headerMessage, Object... headerArgs) { 134 if (headerMessage != null) { 135 mWriter.printf(headerMessage, headerArgs); 136 mWriter.print(". "); 137 } 138 mWriter.println("Available commands:\n"); 139 140 mWriter.increaseIndent(); 141 showCommandHelp("Shows this help message.", 142 CMD_HELP); 143 showCommandHelp("Lists delegated scopes set by the device admin.", 144 CMD_GET_DELEGATED_SCOPES); 145 showCommandHelp("Checks whether uninstalling the given app is blocked.", 146 CMD_IS_UNINSTALL_BLOCKED, "<PKG>"); 147 showCommandHelp("Blocks / unblocks uninstalling the given app.", 148 CMD_SET_UNINSTALL_BLOCKED, "<PKG>", "<true|false>"); 149 showCommandHelp("Generates a device attestation key.", 150 CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR, "<ALIAS>", "[FLAGS]"); 151 showCommandHelp("Post Notification.", 152 CMD_POST_NOTIFICATION, "<MESSAGE>"); 153 showCommandHelp("Post a Toast with the given message and options.", 154 CMD_POST_TOAST, "[" + ARG_VERBOSE + "|" + ARG_VERBOSE_FULL + "]", 155 "[" + ARG_USES_APP_CONTEXT + "]", "[" + ARG_LONG_TOAST + "]", 156 "<MESSAGE>"); 157 showCommandHelp("Enables / Disables the DriveMode Switch in the System UI.", 158 CMD_SET_DRIVE_MODE_SWITCH, "<true|false>"); 159 mWriter.decreaseIndent(); 160 } 161 showCommandHelp(String description, String cmd, String... args)162 private void showCommandHelp(String description, String cmd, String... args) { 163 mWriter.printf("%s", cmd); 164 if (args != null) { 165 for (String arg : args) { 166 mWriter.printf(" %s", arg); 167 } 168 } 169 mWriter.println(":"); 170 mWriter.increaseIndent(); 171 mWriter.printf("%s\n\n", description); 172 mWriter.decreaseIndent(); 173 } 174 getDelegatedScopes()175 private void getDelegatedScopes() { 176 if (!supportDevicePolicyManagement()) return; 177 178 List<String> scopes = mDpm.getDelegatedScopes(/* admin= */ null, mContext.getPackageName()); 179 printCollection("delegated scope", scopes); 180 } 181 isUninstallBlocked()182 private void isUninstallBlocked() { 183 if (!supportDevicePolicyManagement()) return; 184 185 String packageName = getNextArg(); 186 boolean isIt = mDpm.isUninstallBlocked(/* admin= */ null, packageName); 187 mWriter.println(isIt); 188 } 189 setUninstallBlocked()190 private void setUninstallBlocked() { 191 if (!supportDevicePolicyManagement()) return; 192 193 String packageName = getNextArg(); 194 boolean blocked = getNextBooleanArg(); 195 196 Log.i(TAG, "Calling dpm.setUninstallBlocked(" + packageName + ", " + blocked + ")"); 197 mDpm.setUninstallBlocked(/* admin= */ null, packageName, blocked); 198 } 199 generateDeviceAttestationKeyPair()200 private void generateDeviceAttestationKeyPair() { 201 if (!supportDevicePolicyManagement()) return; 202 203 String alias = getNextArg(); 204 int flags = getNextOptionalIntArg(/* defaultValue= */ 0); 205 // Cannot call dpm.generateKeyPair() on main thread 206 warnAboutAsyncCall(); 207 post(() -> handleDeviceAttestationKeyPair(alias, flags)); 208 } 209 handleDeviceAttestationKeyPair(String alias, int flags)210 private void handleDeviceAttestationKeyPair(String alias, int flags) { 211 KeyGenParameterSpec keySpec = buildRsaKeySpecWithKeyAttestation(alias); 212 String algorithm = "RSA"; 213 Log.i(TAG, "calling dpm.generateKeyPair(alg=" + algorithm + ", spec=" + keySpec 214 + ", flags=" + flags + ")"); 215 AttestedKeyPair kp = mDpm.generateKeyPair(/* admin= */ null, algorithm, keySpec, flags); 216 Log.i(TAG, "key: " + kp); 217 } 218 postNotification()219 private void postNotification() { 220 String message = getNextArg(); 221 String channelId = "importance_high"; 222 223 NotificationManager notificationMgr = mContext.getSystemService(NotificationManager.class); 224 notificationMgr.createNotificationChannel( 225 new NotificationChannel(channelId, "Importance High", 226 NotificationManager.IMPORTANCE_HIGH)); 227 Notification notification = new Notification 228 .Builder(mContext, channelId) 229 .setContentTitle("Car Emergency") 230 .setContentText(message) 231 .setCategory(Notification.CATEGORY_CAR_EMERGENCY) 232 .setColor(mContext.getColor(android.R.color.holo_red_light)) 233 .setColorized(true) 234 .setSmallIcon(R.drawable.car_ic_mode) 235 .build(); 236 notificationMgr.notify(mNotificationId, notification); 237 Log.i(TAG, "Post Notification: id=" + mNotificationId + ", message=" + message); 238 } 239 postToast()240 private void postToast() { 241 boolean verbose = false; 242 boolean usesAppContext = false; 243 boolean longToast = false; 244 String messageArg = null; 245 String nextArg = null; 246 247 while ((nextArg = getNextOptioanlArg()) != null) { 248 switch (nextArg) { 249 case ARG_VERBOSE: 250 case ARG_VERBOSE_FULL: 251 verbose = true; 252 break; 253 case ARG_USES_APP_CONTEXT: 254 usesAppContext = true; 255 break; 256 case ARG_LONG_TOAST: 257 longToast = true; 258 break; 259 default: 260 messageArg = nextArg; 261 } 262 } 263 if (messageArg == null) { 264 mWriter.println("Message is required"); 265 return; 266 } 267 268 StringBuilder messageBuilder = new StringBuilder(); 269 Context context = usesAppContext ? mContext.getApplicationContext() : mContext; 270 if (verbose) { 271 messageBuilder.append("user=").append(context.getUserId()) 272 .append(", context=").append(context.getClass().getSimpleName()) 273 .append(", contextDisplay=").append(context.getDisplayId()) 274 .append(", userDisplay=").append(context.getSystemService(UserManager.class) 275 .getMainDisplayIdAssignedToUser()) 276 .append(", length=").append(longToast ? "long" : "short") 277 .append(", message="); 278 279 } 280 String message = messageBuilder.append(messageArg).toString(); 281 Log.i(TAG, "Posting toast: " + message); 282 Toast.makeText(context, message, longToast ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); 283 } 284 setDriveModeSwitch()285 private void setDriveModeSwitch() { 286 boolean value = getNextBooleanArg(); 287 DriveModeSwitchController driveModeSwitchController = new DriveModeSwitchController( 288 mContext 289 ); 290 driveModeSwitchController.setDriveMode(value); 291 } 292 warnAboutAsyncCall()293 private void warnAboutAsyncCall() { 294 mWriter.printf("Command will be executed asynchronally; use `adb logcat %s *:s` for result" 295 + "\n", TAG); 296 } 297 post(Runnable r)298 private void post(Runnable r) { 299 if (mHandler == null) { 300 HandlerThread handlerThread = new HandlerThread("KitchenSinkShellCommandThread"); 301 Log.i(TAG, "Starting " + handlerThread); 302 handlerThread.start(); 303 mHandler = new Handler(handlerThread.getLooper()); 304 } 305 Log.d(TAG, "posting runnable"); 306 mHandler.post(r); 307 } 308 supportDevicePolicyManagement()309 private boolean supportDevicePolicyManagement() { 310 if (mDpm == null) { 311 mWriter.println("Device Policy Management not supported by device"); 312 return false; 313 } 314 return true; 315 } 316 getNextArgAndIncrementCounter()317 private String getNextArgAndIncrementCounter() { 318 return mArgs[++mNextArgIndex]; 319 } 320 321 @Nullable getNextOptioanlArg()322 private String getNextOptioanlArg() { 323 if (++mNextArgIndex >= mArgs.length) { 324 return null; 325 } 326 return mArgs[mNextArgIndex]; 327 } 328 329 getNextArg()330 private String getNextArg() { 331 try { 332 return getNextArgAndIncrementCounter(); 333 } catch (Exception e) { 334 Log.e(TAG, "getNextArg() failed", e); 335 mWriter.println("Error: missing argument"); 336 mWriter.flush(); 337 throw new IllegalArgumentException( 338 "Missing argument. Args=" + Arrays.toString(mArgs), e); 339 } 340 } 341 getNextOptionalIntArg(int defaultValue)342 private int getNextOptionalIntArg(int defaultValue) { 343 try { 344 return Integer.parseInt(getNextArgAndIncrementCounter()); 345 } catch (Exception e) { 346 Log.d(TAG, "Exception getting optional arg: " + e); 347 return defaultValue; 348 } 349 } 350 getNextBooleanArg()351 private boolean getNextBooleanArg() { 352 String arg = getNextArg(); 353 return Boolean.parseBoolean(arg); 354 } 355 printCollection(String nameOnSingular, Collection<String> collection)356 private void printCollection(String nameOnSingular, Collection<String> collection) { 357 if (collection.isEmpty()) { 358 mWriter.printf("No %ss\n", nameOnSingular); 359 return; 360 } 361 int size = collection.size(); 362 mWriter.printf("%d %s%s:\n", size, nameOnSingular, size == 1 ? "" : "s"); 363 collection.forEach((s) -> mWriter.printf(" %s\n", s)); 364 } 365 366 // Copied from CTS' KeyGenerationUtils buildRsaKeySpecWithKeyAttestation(String alias)367 private static KeyGenParameterSpec buildRsaKeySpecWithKeyAttestation(String alias) { 368 return new KeyGenParameterSpec.Builder(alias, 369 KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) 370 .setKeySize(2048) 371 .setDigests(KeyProperties.DIGEST_SHA256) 372 .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS, 373 KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) 374 .setIsStrongBoxBacked(false) 375 .setAttestationChallenge(new byte[] { 376 'a', 'b', 'c' 377 }) 378 .build(); 379 } 380 } 381