blob: 26cc0c10ccb30805d0f4a27c4b30207364168f1d [file] [log] [blame]
Dan Sandler7647f1d2018-11-26 09:56:26 -05001/*
2 * Copyright (C) 2018 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
17package com.android.server.notification;
18
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -080019import android.app.ActivityManager;
Dan Sandler7647f1d2018-11-26 09:56:26 -050020import android.app.INotificationManager;
21import android.app.Notification;
22import android.app.NotificationChannel;
23import android.app.NotificationManager;
24import android.app.PendingIntent;
25import android.app.Person;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.ParceledListSlice;
30import android.content.res.Resources;
31import android.graphics.drawable.BitmapDrawable;
32import android.graphics.drawable.Drawable;
33import android.graphics.drawable.Icon;
34import android.net.Uri;
35import android.os.Binder;
36import android.os.RemoteException;
37import android.os.ShellCommand;
38import android.os.UserHandle;
39import android.text.TextUtils;
40import android.util.Slog;
41
42import java.io.PrintWriter;
43import java.net.URISyntaxException;
44import java.util.Collections;
45
46/**
47 * Implementation of `cmd notification` in NotificationManagerService.
48 */
49public class NotificationShellCmd extends ShellCommand {
50 private static final String USAGE =
51 "usage: cmd notification SUBCMD [args]\n\n"
52 + "SUBCMDs:\n"
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -080053 + " allow_listener COMPONENT [user_id (current user if not specified)]\n"
54 + " disallow_listener COMPONENT [user_id (current user if not specified)]\n"
55 + " allow_assistant COMPONENT [user_id (current user if not specified)]\n"
56 + " remove_assistant COMPONENT [user_id (current user if not specified)]\n"
57 + " allow_dnd PACKAGE [user_id (current user if not specified)]\n"
58 + " disallow_dnd PACKAGE [user_id (current user if not specified)]\n"
Dan Sandler7647f1d2018-11-26 09:56:26 -050059 + " suspend_package PACKAGE\n"
60 + " unsuspend_package PACKAGE\n"
61 + " post [--help | flags] TAG TEXT";
62
63 private static final String NOTIFY_USAGE =
64 "usage: cmd notification post [flags] <tag> <text>\n\n"
65 + "flags:\n"
66 + " -h|--help\n"
67 + " -v|--verbose\n"
68 + " -t|--title <text>\n"
69 + " -i|--icon <iconspec>\n"
70 + " -I|--large-icon <iconspec>\n"
71 + " -S|--style <style> [styleargs]\n"
72 + " -c|--content-intent <intentspec>\n"
73 + "\n"
74 + "styles: (default none)\n"
75 + " bigtext\n"
76 + " bigpicture --picture <iconspec>\n"
77 + " inbox --line <text> --line <text> ...\n"
78 + " messaging --conversation <title> --message <who>:<text> ...\n"
79 + " media\n"
80 + "\n"
81 + "an <iconspec> is one of\n"
82 + " file:///data/local/tmp/<img.png>\n"
83 + " content://<provider>/<path>\n"
84 + " @[<package>:]drawable/<img>\n"
85 + " data:base64,<B64DATA==>\n"
86 + "\n"
87 + "an <intentspec> is (broadcast|service|activity) <args>\n"
88 + " <args> are as described in `am start`";
89
90 public static final int NOTIFICATION_ID = 1138;
91 public static final String NOTIFICATION_PACKAGE = "com.android.shell";
92 public static final String CHANNEL_ID = "shellcmd";
93 public static final String CHANNEL_NAME = "Shell command";
94 public static final int CHANNEL_IMP = NotificationManager.IMPORTANCE_DEFAULT;
95
96 private final NotificationManagerService mDirectService;
97 private final INotificationManager mBinderService;
98
99 public NotificationShellCmd(NotificationManagerService service) {
100 mDirectService = service;
101 mBinderService = service.getBinderService();
102 }
103
104 @Override
105 public int onCommand(String cmd) {
106 if (cmd == null) {
107 return handleDefaultCommands(cmd);
108 }
109 final PrintWriter pw = getOutPrintWriter();
110 try {
111 switch (cmd.replace('-', '_')) {
112 case "allow_dnd": {
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800113 String packageName = getNextArgRequired();
114 int userId = ActivityManager.getCurrentUser();
115 if (peekNextArg() != null) {
116 userId = Integer.parseInt(getNextArgRequired());
117 }
118 mBinderService.setNotificationPolicyAccessGrantedForUser(
119 packageName, userId, true);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500120 }
121 break;
122
123 case "disallow_dnd": {
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800124 String packageName = getNextArgRequired();
125 int userId = ActivityManager.getCurrentUser();
126 if (peekNextArg() != null) {
127 userId = Integer.parseInt(getNextArgRequired());
128 }
129 mBinderService.setNotificationPolicyAccessGrantedForUser(
130 packageName, userId, false);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500131 }
132 break;
133 case "allow_listener": {
134 ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
135 if (cn == null) {
136 pw.println("Invalid listener - must be a ComponentName");
137 return -1;
138 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800139 int userId = ActivityManager.getCurrentUser();
140 if (peekNextArg() != null) {
141 userId = Integer.parseInt(getNextArgRequired());
Dan Sandler7647f1d2018-11-26 09:56:26 -0500142 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800143 mBinderService.setNotificationListenerAccessGrantedForUser(cn, userId, true);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500144 }
145 break;
146 case "disallow_listener": {
147 ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
148 if (cn == null) {
149 pw.println("Invalid listener - must be a ComponentName");
150 return -1;
151 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800152 int userId = ActivityManager.getCurrentUser();
153 if (peekNextArg() != null) {
154 userId = Integer.parseInt(getNextArgRequired());
Dan Sandler7647f1d2018-11-26 09:56:26 -0500155 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800156 mBinderService.setNotificationListenerAccessGrantedForUser(cn, userId, false);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500157 }
158 break;
159 case "allow_assistant": {
160 ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
161 if (cn == null) {
162 pw.println("Invalid assistant - must be a ComponentName");
163 return -1;
164 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800165 int userId = ActivityManager.getCurrentUser();
166 if (peekNextArg() != null) {
167 userId = Integer.parseInt(getNextArgRequired());
168 }
169 mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, true);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500170 }
171 break;
172 case "disallow_assistant": {
173 ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
174 if (cn == null) {
175 pw.println("Invalid assistant - must be a ComponentName");
176 return -1;
177 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800178 int userId = ActivityManager.getCurrentUser();
179 if (peekNextArg() != null) {
180 userId = Integer.parseInt(getNextArgRequired());
181 }
182 mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, false);
Dan Sandler7647f1d2018-11-26 09:56:26 -0500183 }
184 break;
185 case "suspend_package": {
186 // only use for testing
187 mDirectService.simulatePackageSuspendBroadcast(true, getNextArgRequired());
188 }
189 break;
190 case "unsuspend_package": {
191 // only use for testing
192 mDirectService.simulatePackageSuspendBroadcast(false, getNextArgRequired());
193 }
Nicholas Sauer2dcd5e82019-02-14 13:38:04 -0800194 break;
Julia Reynolds0e5a3432019-01-17 09:40:46 -0500195 case "distract_package": {
196 // only use for testing
197 // Flag values are in
198 // {@link android.content.pm.PackageManager.DistractionRestriction}.
199 mDirectService.simulatePackageDistractionBroadcast(
200 Integer.parseInt(getNextArgRequired()),
201 getNextArgRequired().split(","));
202 }
Dan Sandler7647f1d2018-11-26 09:56:26 -0500203 break;
204 case "post":
205 case "notify":
206 doNotify(pw);
207 break;
208 default:
209 return handleDefaultCommands(cmd);
210 }
211 } catch (Exception e) {
212 pw.println("Error occurred. Check logcat for details. " + e.getMessage());
213 Slog.e(NotificationManagerService.TAG, "Error running shell command", e);
214 }
215 return 0;
216 }
217
218 void ensureChannel() throws RemoteException {
219 final int uid = Binder.getCallingUid();
220 final int userid = UserHandle.getCallingUserId();
221 final long token = Binder.clearCallingIdentity();
222 try {
223 if (mBinderService.getNotificationChannelForPackage(NOTIFICATION_PACKAGE,
224 uid, CHANNEL_ID, false) == null) {
225 final NotificationChannel chan = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
226 CHANNEL_IMP);
227 Slog.v(NotificationManagerService.TAG,
228 "creating shell channel for user " + userid + " uid " + uid + ": " + chan);
229 mBinderService.createNotificationChannelsForPackage(NOTIFICATION_PACKAGE, uid,
230 new ParceledListSlice<NotificationChannel>(
231 Collections.singletonList(chan)));
232 Slog.v(NotificationManagerService.TAG, "created channel: "
233 + mBinderService.getNotificationChannelForPackage(NOTIFICATION_PACKAGE,
234 uid, CHANNEL_ID, false));
235 }
236 } finally {
237 Binder.restoreCallingIdentity(token);
238 }
239 }
240
241 Icon parseIcon(Resources res, String encoded) throws IllegalArgumentException {
242 if (TextUtils.isEmpty(encoded)) return null;
243 if (encoded.startsWith("/")) {
244 encoded = "file://" + encoded;
245 }
246 if (encoded.startsWith("http:")
247 || encoded.startsWith("https:")
248 || encoded.startsWith("content:")
249 || encoded.startsWith("file:")
250 || encoded.startsWith("android.resource:")) {
251 Uri asUri = Uri.parse(encoded);
252 return Icon.createWithContentUri(asUri);
253 } else if (encoded.startsWith("@")) {
254 final int resid = res.getIdentifier(encoded.substring(1),
255 "drawable", "android");
256 if (resid != 0) {
257 return Icon.createWithResource(res, resid);
258 }
259 } else if (encoded.startsWith("data:")) {
260 encoded = encoded.substring(encoded.indexOf(',') + 1);
261 byte[] bits = android.util.Base64.decode(encoded, android.util.Base64.DEFAULT);
262 return Icon.createWithData(bits, 0, bits.length);
263 }
264 return null;
265 }
266
267 private int doNotify(PrintWriter pw) throws RemoteException, URISyntaxException {
268 final Context context = mDirectService.getContext();
269 final Resources res = context.getResources();
270 final Notification.Builder builder = new Notification.Builder(context, CHANNEL_ID);
271 String opt;
272
273 boolean verbose = false;
274 Notification.BigPictureStyle bigPictureStyle = null;
275 Notification.BigTextStyle bigTextStyle = null;
276 Notification.InboxStyle inboxStyle = null;
277 Notification.MediaStyle mediaStyle = null;
278 Notification.MessagingStyle messagingStyle = null;
279
280 Icon smallIcon = null;
281 while ((opt = getNextOption()) != null) {
282 boolean large = false;
283 switch (opt) {
284 case "-v":
285 case "--verbose":
286 verbose = true;
287 break;
288 case "-t":
289 case "--title":
290 case "title":
291 builder.setContentTitle(getNextArgRequired());
292 break;
293 case "-I":
294 case "--large-icon":
295 case "--largeicon":
296 case "largeicon":
297 case "large-icon":
298 large = true;
299 // fall through
300 case "-i":
301 case "--icon":
302 case "icon":
303 final String iconSpec = getNextArgRequired();
304 final Icon icon = parseIcon(res, iconSpec);
305 if (icon == null) {
306 pw.println("error: invalid icon: " + iconSpec);
307 return -1;
308 }
309 if (large) {
310 builder.setLargeIcon(icon);
311 large = false;
312 } else {
313 smallIcon = icon;
314 }
315 break;
316 case "-c":
317 case "--content-intent":
318 case "content-intent":
319 case "--intent":
320 case "intent":
321 String intentKind = null;
322 switch (peekNextArg()) {
323 case "broadcast":
324 case "service":
325 case "activity":
326 intentKind = getNextArg();
327 }
328 final Intent intent = Intent.parseCommandArgs(this, null);
329 if (intent.getData() == null) {
330 // force unique intents unless you know what you're doing
331 intent.setData(Uri.parse("xyz:" + System.currentTimeMillis()));
332 }
333 final PendingIntent pi;
334 if ("broadcast".equals(intentKind)) {
335 pi = PendingIntent.getBroadcastAsUser(
336 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT,
337 UserHandle.CURRENT);
338 } else if ("service".equals(intentKind)) {
339 pi = PendingIntent.getService(
340 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
341 } else {
342 pi = PendingIntent.getActivityAsUser(
343 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, null,
344 UserHandle.CURRENT);
345 }
346 builder.setContentIntent(pi);
347 break;
348 case "-S":
349 case "--style":
350 final String styleSpec = getNextArgRequired().toLowerCase();
351 switch (styleSpec) {
352 case "bigtext":
353 bigTextStyle = new Notification.BigTextStyle();
354 builder.setStyle(bigTextStyle);
355 break;
356 case "bigpicture":
357 bigPictureStyle = new Notification.BigPictureStyle();
358 builder.setStyle(bigPictureStyle);
359 break;
360 case "inbox":
361 inboxStyle = new Notification.InboxStyle();
362 builder.setStyle(inboxStyle);
363 break;
364 case "messaging":
365 String name = "You";
366 if ("--user".equals(peekNextArg())) {
367 getNextArg();
368 name = getNextArgRequired();
369 }
370 messagingStyle = new Notification.MessagingStyle(
371 new Person.Builder().setName(name).build());
372 builder.setStyle(messagingStyle);
373 break;
374 case "media":
375 mediaStyle = new Notification.MediaStyle();
376 builder.setStyle(mediaStyle);
377 break;
378 default:
379 throw new IllegalArgumentException(
380 "unrecognized notification style: " + styleSpec);
381 }
382 break;
383 case "--bigText": case "--bigtext": case "--big-text":
384 if (bigTextStyle == null) {
385 throw new IllegalArgumentException("--bigtext requires --style bigtext");
386 }
387 bigTextStyle.bigText(getNextArgRequired());
388 break;
389 case "--picture":
390 if (bigPictureStyle == null) {
391 throw new IllegalArgumentException("--picture requires --style bigpicture");
392 }
393 final String pictureSpec = getNextArgRequired();
394 final Icon pictureAsIcon = parseIcon(res, pictureSpec);
395 if (pictureAsIcon == null) {
396 throw new IllegalArgumentException("bad picture spec: " + pictureSpec);
397 }
398 final Drawable d = pictureAsIcon.loadDrawable(context);
399 if (d instanceof BitmapDrawable) {
400 bigPictureStyle.bigPicture(((BitmapDrawable) d).getBitmap());
401 } else {
402 throw new IllegalArgumentException("not a bitmap: " + pictureSpec);
403 }
404 break;
405 case "--line":
406 if (inboxStyle == null) {
407 throw new IllegalArgumentException("--line requires --style inbox");
408 }
409 inboxStyle.addLine(getNextArgRequired());
410 break;
411 case "--message":
412 if (messagingStyle == null) {
413 throw new IllegalArgumentException(
414 "--message requires --style messaging");
415 }
416 String arg = getNextArgRequired();
417 String[] parts = arg.split(":", 2);
418 if (parts.length > 1) {
419 messagingStyle.addMessage(parts[1], System.currentTimeMillis(),
420 parts[0]);
421 } else {
422 messagingStyle.addMessage(parts[0], System.currentTimeMillis(),
423 new String[]{
424 messagingStyle.getUserDisplayName().toString(),
425 "Them"
426 }[messagingStyle.getMessages().size() % 2]);
427 }
428 break;
429 case "--conversation":
430 if (messagingStyle == null) {
431 throw new IllegalArgumentException(
432 "--conversation requires --style messaging");
433 }
434 messagingStyle.setConversationTitle(getNextArgRequired());
435 break;
436 case "-h":
437 case "--help":
438 case "--wtf":
439 default:
440 pw.println(NOTIFY_USAGE);
441 return 0;
442 }
443 }
444
445 final String tag = getNextArg();
446 final String text = getNextArg();
447 if (tag == null || text == null) {
448 pw.println(NOTIFY_USAGE);
449 return -1;
450 }
451
452 builder.setContentText(text);
453
454 if (smallIcon == null) {
455 // uh oh, let's substitute something
456 builder.setSmallIcon(com.android.internal.R.drawable.stat_notify_chat);
457 } else {
458 builder.setSmallIcon(smallIcon);
459 }
460
461 ensureChannel();
462
463 final Notification n = builder.build();
464 pw.println("posting:\n " + n);
465 Slog.v("NotificationManager", "posting: " + n);
466
467 final int userId = UserHandle.getCallingUserId();
468 final long token = Binder.clearCallingIdentity();
469 try {
470 mBinderService.enqueueNotificationWithTag(
471 NOTIFICATION_PACKAGE, "android",
472 tag, NOTIFICATION_ID,
473 n, userId);
474 } finally {
475 Binder.restoreCallingIdentity(token);
476 }
477
478 if (verbose) {
479 NotificationRecord nr = mDirectService.findNotificationLocked(
480 NOTIFICATION_PACKAGE, tag, NOTIFICATION_ID, userId);
481 for (int tries = 3; tries-- > 0; ) {
482 if (nr != null) break;
483 try {
484 pw.println("waiting for notification to post...");
485 Thread.sleep(500);
486 } catch (InterruptedException e) {
487 }
488 nr = mDirectService.findNotificationLocked(
489 NOTIFICATION_PACKAGE, tag, NOTIFICATION_ID, userId);
490 }
491 if (nr == null) {
492 pw.println("warning: couldn't find notification after enqueueing");
493 } else {
494 pw.println("posted: ");
495 nr.dump(pw, " ", context, false);
496 }
497 }
498
499 return 0;
500 }
501
502 @Override
503 public void onHelp() {
504 getOutPrintWriter().println(USAGE);
505 }
506}
507