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