• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.server.notification;
18 
19 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
20 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
21 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
22 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
23 import static android.app.NotificationManager.INTERRUPTION_FILTER_UNKNOWN;
24 
25 import android.annotation.SuppressLint;
26 import android.app.ActivityManager;
27 import android.app.INotificationManager;
28 import android.app.Notification;
29 import android.app.NotificationChannel;
30 import android.app.NotificationManager;
31 import android.app.PendingIntent;
32 import android.app.Person;
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.pm.PackageManager;
37 import android.content.pm.ParceledListSlice;
38 import android.content.res.Resources;
39 import android.graphics.drawable.BitmapDrawable;
40 import android.graphics.drawable.Drawable;
41 import android.graphics.drawable.Icon;
42 import android.net.Uri;
43 import android.os.Binder;
44 import android.os.Process;
45 import android.os.RemoteException;
46 import android.os.ShellCommand;
47 import android.os.UserHandle;
48 import android.provider.Settings;
49 import android.service.notification.NotificationListenerService;
50 import android.service.notification.StatusBarNotification;
51 import android.text.TextUtils;
52 import android.util.Slog;
53 
54 import java.io.PrintWriter;
55 import java.net.URISyntaxException;
56 import java.util.Collections;
57 import java.util.Date;
58 
59 /**
60  * Implementation of `cmd notification` in NotificationManagerService.
61  */
62 public class NotificationShellCmd extends ShellCommand {
63     private static final String TAG = "NotifShellCmd";
64     private static final String USAGE = "usage: cmd notification SUBCMD [args]\n\n"
65             + "SUBCMDs:\n"
66             + "  allow_listener COMPONENT [user_id (current user if not specified)]\n"
67             + "  disallow_listener COMPONENT [user_id (current user if not specified)]\n"
68             + "  allow_assistant COMPONENT [user_id (current user if not specified)]\n"
69             + "  remove_assistant COMPONENT [user_id (current user if not specified)]\n"
70             + "  set_dnd [on|none (same as on)|priority|alarms|all|off (same as all)]\n"
71             + "  allow_dnd PACKAGE [user_id (current user if not specified)]\n"
72             + "  disallow_dnd PACKAGE [user_id (current user if not specified)]\n"
73             + "  reset_assistant_user_set [user_id (current user if not specified)]\n"
74             + "  get_approved_assistant [user_id (current user if not specified)]\n"
75             + "  post [--help | flags] TAG TEXT\n"
76             + "  set_bubbles PACKAGE PREFERENCE (0=none 1=all 2=selected) "
77                     + "[user_id (current user if not specified)]\n"
78             + "  set_bubbles_channel PACKAGE CHANNEL_ID ALLOW "
79                     + "[user_id (current user if not specified)]\n"
80             + "  list\n"
81             + "  get <notification-key>\n"
82             + "  snooze --for <msec> <notification-key>\n"
83             + "  unsnooze <notification-key>\n"
84             + "  set_exempt_th_force_grouping [true|false]\n"
85             + "  redact_otp_from_untrusted_listeners [true|false]\n"
86             ;
87 
88     private static final String NOTIFY_USAGE =
89               "usage: cmd notification post [flags] <tag> <text>\n\n"
90             + "flags:\n"
91             + "  -h|--help\n"
92             + "  -v|--verbose\n"
93             + "  -t|--title <text>\n"
94             + "  -i|--icon <iconspec>\n"
95             + "  -I|--large-icon <iconspec>\n"
96             + "  -S|--style <style> [styleargs]\n"
97             + "  -c|--content-intent <intentspec>\n"
98             + "\n"
99             + "styles: (default none)\n"
100             + "  bigtext\n"
101             + "  bigpicture --picture <iconspec>\n"
102             + "  inbox --line <text> --line <text> ...\n"
103             + "  messaging --conversation <title> --message <who>:<text> ...\n"
104             + "  media\n"
105             + "\n"
106             + "an <iconspec> is one of\n"
107             + "  file:///data/local/tmp/<img.png>\n"
108             + "  content://<provider>/<path>\n"
109             + "  @[<package>:]drawable/<img>\n"
110             + "  data:base64,<B64DATA==>\n"
111             + "\n"
112             + "an <intentspec> is (broadcast|service|activity) <args>\n"
113             + "  <args> are as described in `am start`";
114 
115     public static final int NOTIFICATION_ID = 2020;
116     public static final String CHANNEL_ID = "shell_cmd";
117     public static final String CHANNEL_NAME = "Shell command";
118     public static final int CHANNEL_IMP = NotificationManager.IMPORTANCE_DEFAULT;
119 
120     private final NotificationManagerService mDirectService;
121     private final INotificationManager mBinderService;
122     private final PackageManager mPm;
123 
NotificationShellCmd(NotificationManagerService service)124     public NotificationShellCmd(NotificationManagerService service) {
125         mDirectService = service;
126         mBinderService = service.getBinderService();
127         mPm = mDirectService.getContext().getPackageManager();
128     }
129 
checkShellCommandPermission(int callingUid)130     protected boolean checkShellCommandPermission(int callingUid) {
131         return (callingUid == Process.ROOT_UID || callingUid == Process.SHELL_UID);
132     }
133 
134     @Override
onCommand(String cmd)135     public int onCommand(String cmd) {
136         if (cmd == null) {
137             return handleDefaultCommands(cmd);
138         }
139         String callingPackage = null;
140         final int callingUid = Binder.getCallingUid();
141         final long identity = Binder.clearCallingIdentity();
142         try {
143             if (callingUid == Process.ROOT_UID) {
144                 callingPackage = NotificationManagerService.ROOT_PKG;
145             } else {
146                 String[] packages = mPm.getPackagesForUid(callingUid);
147                 if (packages != null && packages.length > 0) {
148                     callingPackage = packages[0];
149                 }
150             }
151         } catch (Exception e) {
152             Slog.e(TAG, "failed to get caller pkg", e);
153         } finally {
154             Binder.restoreCallingIdentity(identity);
155         }
156 
157         final PrintWriter pw = getOutPrintWriter();
158 
159         if (!checkShellCommandPermission(callingUid)) {
160             Slog.e(TAG, "error: permission denied: callingUid="
161                     + callingUid + " callingPackage=" + callingPackage);
162             pw.println("error: permission denied: callingUid="
163                     + callingUid + " callingPackage=" + callingPackage);
164             return 255;
165         }
166 
167         try {
168             switch (cmd.replace('-', '_')) {
169                 case "set_dnd": {
170                     String mode = getNextArgRequired();
171                     int interruptionFilter = INTERRUPTION_FILTER_UNKNOWN;
172                     switch(mode) {
173                         case "none":
174                         case "on":
175                             interruptionFilter = INTERRUPTION_FILTER_NONE;
176                             break;
177                         case "priority":
178                             interruptionFilter = INTERRUPTION_FILTER_PRIORITY;
179                             break;
180                         case "alarms":
181                             interruptionFilter = INTERRUPTION_FILTER_ALARMS;
182                             break;
183                         case "all":
184                         case "off":
185                             interruptionFilter = INTERRUPTION_FILTER_ALL;
186                     }
187                     final int filter = interruptionFilter;
188                     mBinderService.setInterruptionFilter(callingPackage, filter,
189                             /* fromUser= */ true);
190                 }
191                 break;
192                 case "allow_dnd": {
193                     String packageName = getNextArgRequired();
194                     int userId = ActivityManager.getCurrentUser();
195                     if (peekNextArg() != null) {
196                         userId = Integer.parseInt(getNextArgRequired());
197                     }
198                     mBinderService.setNotificationPolicyAccessGrantedForUser(
199                             packageName, userId, true);
200                 }
201                 break;
202 
203                 case "disallow_dnd": {
204                     String packageName = getNextArgRequired();
205                     int userId = ActivityManager.getCurrentUser();
206                     if (peekNextArg() != null) {
207                         userId = Integer.parseInt(getNextArgRequired());
208                     }
209                     mBinderService.setNotificationPolicyAccessGrantedForUser(
210                             packageName, userId, false);
211                 }
212                 break;
213                 case "allow_listener": {
214                     ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
215                     if (cn == null) {
216                         pw.println("Invalid listener - must be a ComponentName");
217                         return -1;
218                     }
219                     int userId = ActivityManager.getCurrentUser();
220                     if (peekNextArg() != null) {
221                         userId = Integer.parseInt(getNextArgRequired());
222                     }
223                     mBinderService.setNotificationListenerAccessGrantedForUser(
224                             cn, userId, true, true);
225                 }
226                 break;
227                 case "disallow_listener": {
228                     ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
229                     if (cn == null) {
230                         pw.println("Invalid listener - must be a ComponentName");
231                         return -1;
232                     }
233                     int userId = ActivityManager.getCurrentUser();
234                     if (peekNextArg() != null) {
235                         userId = Integer.parseInt(getNextArgRequired());
236                     }
237                     mBinderService.setNotificationListenerAccessGrantedForUser(
238                             cn, userId, false, true);
239                 }
240                 break;
241                 case "allow_assistant": {
242                     ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
243                     if (cn == null) {
244                         pw.println("Invalid assistant - must be a ComponentName");
245                         return -1;
246                     }
247                     int userId = ActivityManager.getCurrentUser();
248                     if (peekNextArg() != null) {
249                         userId = Integer.parseInt(getNextArgRequired());
250                     }
251                     mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, true);
252                 }
253                 break;
254                 case "disallow_assistant": {
255                     ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
256                     if (cn == null) {
257                         pw.println("Invalid assistant - must be a ComponentName");
258                         return -1;
259                     }
260                     int userId = ActivityManager.getCurrentUser();
261                     if (peekNextArg() != null) {
262                         userId = Integer.parseInt(getNextArgRequired());
263                     }
264                     mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, false);
265                 }
266                 break;
267                 case "reset_assistant_user_set": {
268                     int userId = ActivityManager.getCurrentUser();
269                     if (peekNextArg() != null) {
270                         userId = Integer.parseInt(getNextArgRequired());
271                     }
272                     mDirectService.resetAssistantUserSet(userId);
273                     break;
274                 }
275                 case "get_approved_assistant": {
276                     int userId = ActivityManager.getCurrentUser();
277                     if (peekNextArg() != null) {
278                         userId = Integer.parseInt(getNextArgRequired());
279                     }
280                     ComponentName approvedAssistant = mDirectService.getApprovedAssistant(userId);
281                     if (approvedAssistant == null) {
282                         pw.println("null");
283                     } else {
284                         pw.println(approvedAssistant.flattenToString());
285                     }
286                     break;
287                 }
288                 case "set_bubbles": {
289                     // only use for testing
290                     String packageName = getNextArgRequired();
291                     int preference = Integer.parseInt(getNextArgRequired());
292                     if (preference > 3 || preference < 0) {
293                         pw.println("Invalid preference - must be between 0-3 "
294                                 + "(0=none 1=all 2=selected)");
295                         return -1;
296                     }
297                     int userId = ActivityManager.getCurrentUser();
298                     if (peekNextArg() != null) {
299                         userId = Integer.parseInt(getNextArgRequired());
300                     }
301                     int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0));
302                     mBinderService.setBubblesAllowed(packageName, appUid, preference);
303                     break;
304                 }
305                 case "set_bubbles_channel": {
306                     // only use for testing
307                     String packageName = getNextArgRequired();
308                     String channelId = getNextArgRequired();
309                     boolean allow = Boolean.parseBoolean(getNextArgRequired());
310                     int userId = ActivityManager.getCurrentUser();
311                     if (peekNextArg() != null) {
312                         userId = Integer.parseInt(getNextArgRequired());
313                     }
314                     NotificationChannel channel = mBinderService.getNotificationChannel(
315                             callingPackage, userId, packageName, channelId);
316                     channel.setAllowBubbles(allow);
317                     int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0));
318                     mBinderService.updateNotificationChannelForPackage(packageName, appUid,
319                             channel);
320                     break;
321                 }
322                 case "post":
323                 case "notify":
324                     doNotify(pw, callingPackage, callingUid);
325                     break;
326                 case "list":
327                     for (String key : mDirectService.mNotificationsByKey.keySet()) {
328                         pw.println(key);
329                     }
330                     break;
331                 case "get": {
332                     final String key = getNextArgRequired();
333                     final NotificationRecord nr = mDirectService.getNotificationRecord(key);
334                     if (nr != null) {
335                         nr.dump(pw, "", mDirectService.getContext(), false);
336                     } else {
337                         pw.println("error: no active notification matching key: " + key);
338                         return 1;
339                     }
340                     break;
341                 }
342                 case "snoozed": {
343                     final StringBuilder sb = new StringBuilder();
344                     final SnoozeHelper sh = mDirectService.mSnoozeHelper;
345                     for (NotificationRecord nr : sh.getSnoozed()) {
346                         final String pkg = nr.getSbn().getPackageName();
347                         final String key = nr.getKey();
348                         pw.println(key + " snoozed, time="
349                                 + sh.getSnoozeTimeForUnpostedNotification(
350                                         nr.getUserId(), pkg, key)
351                                 + " context="
352                                 + sh.getSnoozeContextForUnpostedNotification(
353                                         nr.getUserId(), pkg, key));
354                     }
355                     break;
356                 }
357                 case "unsnooze": {
358                     boolean mute = false;
359                     String key = getNextArgRequired();
360                     if ("--mute".equals(key)) {
361                         mute = true;
362                         key = getNextArgRequired();
363                     }
364                     if (null != mDirectService.mSnoozeHelper.getNotification(key)) {
365                         pw.println("unsnoozing: " + key);
366                         mDirectService.unsnoozeNotificationInt(key, null, mute);
367                     } else {
368                         pw.println("error: no snoozed otification matching key: " + key);
369                         return 1;
370                     }
371                     break;
372                 }
373                 case "snooze": {
374                     String subflag = getNextArg();
375                     if (subflag == null) {
376                         subflag = "help";
377                     } else if (subflag.startsWith("--")) {
378                         subflag = subflag.substring(2);
379                     }
380                     String flagarg = getNextArg();
381                     String key = getNextArg();
382                     if (key == null) subflag = "help";
383                     String criterion = null;
384                     long duration = 0;
385                     switch (subflag) {
386                         case "context":
387                         case "condition":
388                         case "criterion":
389                             criterion = flagarg;
390                             break;
391                         case "until":
392                         case "for":
393                         case "duration":
394                             duration = Long.parseLong(flagarg);
395                             break;
396                         default:
397                             pw.println("usage: cmd notification snooze (--for <msec> | "
398                                     + "--context <snooze-criterion-id>) <key>");
399                             return 1;
400                     }
401                     if (duration > 0 || criterion != null) {
402                         ShellNls nls = new ShellNls();
403                         nls.registerAsSystemService(mDirectService.getContext(),
404                                 new ComponentName(nls.getClass().getPackageName(),
405                                         nls.getClass().getName()),
406                                 ActivityManager.getCurrentUser());
407                         if (!waitForBind(nls)) {
408                             pw.println("error: could not bind a listener in time");
409                             return 1;
410                         }
411                         if (duration > 0) {
412                             pw.println(String.format("snoozing <%s> until time: %s", key,
413                                     new Date(System.currentTimeMillis() + duration)));
414                             nls.snoozeNotification(key, duration);
415                         } else {
416                             pw.println(String.format("snoozing <%s> until criterion: %s", key,
417                                     criterion));
418                             nls.snoozeNotification(key, criterion);
419                         }
420                         waitForSnooze(nls, key);
421                         nls.unregisterAsSystemService();
422                         waitForUnbind(nls);
423                     } else {
424                         pw.println("error: invalid value for --" + subflag + ": " + flagarg);
425                         return 1;
426                     }
427                     break;
428                 }
429                 case "set_exempt_th_force_grouping": {
430                     String arg = getNextArgRequired();
431                     final boolean exemptTestHarnessFromForceGrouping =
432                             "true".equals(arg) || "1".equals(arg);
433                     mDirectService.setTestHarnessExempted(exemptTestHarnessFromForceGrouping);
434                     break;
435                 }
436                 case "redact_otp_from_untrusted_listeners": {
437                     String arg = getNextArgRequired();
438                     final int allow = "true".equals(arg) || "1".equals(arg) ? 1 : 0;
439                     Settings.Global.putInt(mDirectService.getContext().getContentResolver(),
440                             Settings.Global.REDACT_OTP_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS,
441                             allow);
442                     break;
443                 }
444                 default:
445                     return handleDefaultCommands(cmd);
446             }
447         } catch (Exception e) {
448             pw.println("Error occurred. Check logcat for details. " + e.getMessage());
449             Slog.e(NotificationManagerService.TAG, "Error running shell command", e);
450         }
451         return 0;
452     }
453 
ensureChannel(String callingPackage, int callingUid)454     void ensureChannel(String callingPackage, int callingUid) throws RemoteException {
455         final NotificationChannel channel =
456                 new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, CHANNEL_IMP);
457         mBinderService.createNotificationChannels(callingPackage,
458                 new ParceledListSlice<>(Collections.singletonList(channel)));
459         Slog.v(NotificationManagerService.TAG, "created channel: "
460                 + mBinderService.getNotificationChannel(callingPackage,
461                 UserHandle.getUserId(callingUid), callingPackage, CHANNEL_ID));
462     }
463 
parseIcon(Resources res, String encoded)464     Icon parseIcon(Resources res, String encoded) throws IllegalArgumentException {
465         if (TextUtils.isEmpty(encoded)) return null;
466         if (encoded.startsWith("/")) {
467             encoded = "file://" + encoded;
468         }
469         if (encoded.startsWith("http:")
470                 || encoded.startsWith("https:")
471                 || encoded.startsWith("content:")
472                 || encoded.startsWith("file:")
473                 || encoded.startsWith("android.resource:")) {
474             Uri asUri = Uri.parse(encoded);
475             return Icon.createWithContentUri(asUri);
476         } else if (encoded.startsWith("@")) {
477             final int resid = res.getIdentifier(encoded.substring(1),
478                     "drawable", "android");
479             if (resid != 0) {
480                 return Icon.createWithResource(res, resid);
481             }
482         } else if (encoded.startsWith("data:")) {
483             encoded = encoded.substring(encoded.indexOf(',') + 1);
484             byte[] bits = android.util.Base64.decode(encoded, android.util.Base64.DEFAULT);
485             return Icon.createWithData(bits, 0, bits.length);
486         }
487         return null;
488     }
489 
doNotify(PrintWriter pw, String callingPackage, int callingUid)490     private int doNotify(PrintWriter pw, String callingPackage, int callingUid)
491             throws RemoteException, URISyntaxException {
492         final Context context = mDirectService.getContext();
493         final Resources res = context.getResources();
494         final Notification.Builder builder = new Notification.Builder(context, CHANNEL_ID);
495         String opt;
496 
497         boolean verbose = false;
498         Notification.BigPictureStyle bigPictureStyle = null;
499         Notification.BigTextStyle bigTextStyle = null;
500         Notification.InboxStyle inboxStyle = null;
501         Notification.MediaStyle mediaStyle = null;
502         Notification.MessagingStyle messagingStyle = null;
503 
504         Icon smallIcon = null;
505         while ((opt = getNextOption()) != null) {
506             boolean large = false;
507             switch (opt) {
508                 case "-v":
509                 case "--verbose":
510                     verbose = true;
511                     break;
512                 case "-t":
513                 case "--title":
514                 case "title":
515                     builder.setContentTitle(getNextArgRequired());
516                     break;
517                 case "-I":
518                 case "--large-icon":
519                 case "--largeicon":
520                 case "largeicon":
521                 case "large-icon":
522                     large = true;
523                     // fall through
524                 case "-i":
525                 case "--icon":
526                 case "icon":
527                     final String iconSpec = getNextArgRequired();
528                     final Icon icon = parseIcon(res, iconSpec);
529                     if (icon == null) {
530                         pw.println("error: invalid icon: " + iconSpec);
531                         return -1;
532                     }
533                     if (large) {
534                         builder.setLargeIcon(icon);
535                         large = false;
536                     } else {
537                         smallIcon = icon;
538                     }
539                     break;
540                 case "-c":
541                 case "--content-intent":
542                 case "content-intent":
543                 case "--intent":
544                 case "intent":
545                     String intentKind = null;
546                     switch (peekNextArg()) {
547                         case "broadcast":
548                         case "service":
549                         case "activity":
550                             intentKind = getNextArg();
551                     }
552                     final Intent intent = Intent.parseCommandArgs(this, null);
553                     if (intent.getData() == null) {
554                         // force unique intents unless you know what you're doing
555                         intent.setData(Uri.parse("xyz:" + System.currentTimeMillis()));
556                     }
557                     final PendingIntent pi;
558                     if ("broadcast".equals(intentKind)) {
559                         pi = PendingIntent.getBroadcastAsUser(
560                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
561                                         | PendingIntent.FLAG_IMMUTABLE,
562                                 UserHandle.CURRENT);
563                     } else if ("service".equals(intentKind)) {
564                         pi = PendingIntent.getService(
565                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
566                                         | PendingIntent.FLAG_IMMUTABLE);
567                     } else {
568                         pi = PendingIntent.getActivityAsUser(
569                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
570                                         | PendingIntent.FLAG_IMMUTABLE, null,
571                                 UserHandle.CURRENT);
572                     }
573                     builder.setContentIntent(pi);
574                     break;
575                 case "-S":
576                 case "--style":
577                     final String styleSpec = getNextArgRequired().toLowerCase();
578                     switch (styleSpec) {
579                         case "bigtext":
580                             bigTextStyle = new Notification.BigTextStyle();
581                             builder.setStyle(bigTextStyle);
582                             break;
583                         case "bigpicture":
584                             bigPictureStyle = new Notification.BigPictureStyle();
585                             builder.setStyle(bigPictureStyle);
586                             break;
587                         case "inbox":
588                             inboxStyle = new Notification.InboxStyle();
589                             builder.setStyle(inboxStyle);
590                             break;
591                         case "messaging":
592                             String name = "You";
593                             if ("--user".equals(peekNextArg())) {
594                                 getNextArg();
595                                 name = getNextArgRequired();
596                             }
597                             messagingStyle = new Notification.MessagingStyle(
598                                     new Person.Builder().setName(name).build());
599                             builder.setStyle(messagingStyle);
600                             break;
601                         case "media":
602                             mediaStyle = new Notification.MediaStyle();
603                             builder.setStyle(mediaStyle);
604                             break;
605                         default:
606                             throw new IllegalArgumentException(
607                                     "unrecognized notification style: " + styleSpec);
608                     }
609                     break;
610                 case "--bigText": case "--bigtext": case "--big-text":
611                     if (bigTextStyle == null) {
612                         throw new IllegalArgumentException("--bigtext requires --style bigtext");
613                     }
614                     bigTextStyle.bigText(getNextArgRequired());
615                     break;
616                 case "--picture":
617                     if (bigPictureStyle == null) {
618                         throw new IllegalArgumentException("--picture requires --style bigpicture");
619                     }
620                     final String pictureSpec = getNextArgRequired();
621                     final Icon pictureAsIcon = parseIcon(res, pictureSpec);
622                     if (pictureAsIcon == null) {
623                         throw new IllegalArgumentException("bad picture spec: " + pictureSpec);
624                     }
625                     final Drawable d = pictureAsIcon.loadDrawable(context);
626                     if (d instanceof BitmapDrawable) {
627                         bigPictureStyle.bigPicture(((BitmapDrawable) d).getBitmap());
628                     } else {
629                         throw new IllegalArgumentException("not a bitmap: " + pictureSpec);
630                     }
631                     break;
632                 case "--line":
633                     if (inboxStyle == null) {
634                         throw new IllegalArgumentException("--line requires --style inbox");
635                     }
636                     inboxStyle.addLine(getNextArgRequired());
637                     break;
638                 case "--message":
639                     if (messagingStyle == null) {
640                         throw new IllegalArgumentException(
641                                 "--message requires --style messaging");
642                     }
643                     String arg = getNextArgRequired();
644                     String[] parts = arg.split(":", 2);
645                     if (parts.length > 1) {
646                         messagingStyle.addMessage(parts[1], System.currentTimeMillis(),
647                                 parts[0]);
648                     } else {
649                         messagingStyle.addMessage(parts[0], System.currentTimeMillis(),
650                                 new String[]{
651                                         messagingStyle.getUserDisplayName().toString(),
652                                         "Them"
653                                 }[messagingStyle.getMessages().size() % 2]);
654                     }
655                     break;
656                 case "--conversation":
657                     if (messagingStyle == null) {
658                         throw new IllegalArgumentException(
659                                 "--conversation requires --style messaging");
660                     }
661                     messagingStyle.setConversationTitle(getNextArgRequired());
662                     break;
663                 case "-h":
664                 case "--help":
665                 case "--wtf":
666                 default:
667                     pw.println(NOTIFY_USAGE);
668                     return 0;
669             }
670         }
671 
672         final String tag = getNextArg();
673         final String text = getNextArg();
674         if (tag == null || text == null) {
675             pw.println(NOTIFY_USAGE);
676             return -1;
677         }
678 
679         builder.setContentText(text);
680 
681         if (smallIcon == null) {
682             // uh oh, let's substitute something
683             builder.setSmallIcon(com.android.internal.R.drawable.stat_notify_chat);
684         } else {
685             builder.setSmallIcon(smallIcon);
686         }
687 
688         ensureChannel(callingPackage, callingUid);
689 
690         final Notification n = builder.build();
691         pw.println("posting:\n  " + n);
692         Slog.v("NotificationManager", "posting: " + n);
693 
694         mBinderService.enqueueNotificationWithTag(callingPackage, callingPackage, tag,
695                 NOTIFICATION_ID, n, UserHandle.getUserId(callingUid));
696 
697         if (verbose) {
698             NotificationRecord nr = mDirectService.findNotificationLocked(
699                     callingPackage, tag, NOTIFICATION_ID, UserHandle.getUserId(callingUid));
700             for (int tries = 3; tries-- > 0; ) {
701                 if (nr != null) break;
702                 try {
703                     pw.println("waiting for notification to post...");
704                     Thread.sleep(500);
705                 } catch (InterruptedException e) {
706                 }
707                 nr = mDirectService.findNotificationLocked(
708                         callingPackage, tag, NOTIFICATION_ID, UserHandle.getUserId(callingUid));
709             }
710             if (nr == null) {
711                 pw.println("warning: couldn't find notification after enqueueing");
712             } else {
713                 pw.println("posted: ");
714                 nr.dump(pw, "  ", context, false);
715             }
716         }
717 
718         return 0;
719     }
720 
waitForSnooze(ShellNls nls, String key)721     private void waitForSnooze(ShellNls nls, String key) {
722         for (int i = 0; i < 20; i++) {
723             StatusBarNotification[] sbns = nls.getSnoozedNotifications();
724             for (StatusBarNotification sbn : sbns) {
725                 if (sbn.getKey().equals(key)) {
726                     return;
727                 }
728             }
729             try {
730                 Thread.sleep(100);
731             } catch (InterruptedException e) {
732                 e.printStackTrace();
733             }
734         }
735         return;
736     }
737 
waitForBind(ShellNls nls)738     private boolean waitForBind(ShellNls nls) {
739         for (int i = 0; i < 20; i++) {
740             if (nls.isConnected) {
741                 Slog.i(TAG, "Bound Shell NLS");
742                 return true;
743             } else {
744                 try {
745                     Thread.sleep(100);
746                 } catch (InterruptedException e) {
747                     e.printStackTrace();
748                 }
749             }
750         }
751         return false;
752     }
753 
waitForUnbind(ShellNls nls)754     private void waitForUnbind(ShellNls nls) {
755         for (int i = 0; i < 10; i++) {
756             if (!nls.isConnected) {
757                 Slog.i(TAG, "Unbound Shell NLS");
758                 return;
759             } else {
760                 try {
761                     Thread.sleep(100);
762                 } catch (InterruptedException e) {
763                     e.printStackTrace();
764                 }
765             }
766         }
767     }
768 
769     @Override
onHelp()770     public void onHelp() {
771         getOutPrintWriter().println(USAGE);
772     }
773 
774     @SuppressLint("OverrideAbstract")
775     private static class ShellNls extends NotificationListenerService {
776         private static ShellNls
777                 sNotificationListenerInstance = null;
778         boolean isConnected;
779 
780         @Override
onListenerConnected()781         public void onListenerConnected() {
782             super.onListenerConnected();
783             sNotificationListenerInstance = this;
784             isConnected = true;
785         }
786         @Override
onListenerDisconnected()787         public void onListenerDisconnected() {
788             isConnected = false;
789         }
790 
getInstance()791         public static ShellNls getInstance() {
792             return sNotificationListenerInstance;
793         }
794     }
795 }
796 
797