• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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