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