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