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