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