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