• 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 
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