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