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