1 /* 2 * Copyright (C) 2020 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 package com.android.wm.shell.bubbles; 17 18 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 19 import static android.os.AsyncTask.Status.FINISHED; 20 21 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 22 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; 23 24 import android.annotation.DimenRes; 25 import android.annotation.Hide; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.app.Notification; 29 import android.app.PendingIntent; 30 import android.app.Person; 31 import android.app.TaskInfo; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.LocusId; 35 import android.content.pm.ApplicationInfo; 36 import android.content.pm.PackageManager; 37 import android.content.pm.ShortcutInfo; 38 import android.content.res.Resources; 39 import android.graphics.Bitmap; 40 import android.graphics.Path; 41 import android.graphics.drawable.Drawable; 42 import android.graphics.drawable.Icon; 43 import android.os.Parcelable; 44 import android.os.UserHandle; 45 import android.provider.Settings; 46 import android.service.notification.StatusBarNotification; 47 import android.text.TextUtils; 48 import android.util.Log; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.internal.logging.InstanceId; 52 import com.android.internal.protolog.ProtoLog; 53 import com.android.launcher3.icons.BubbleIconFactory; 54 import com.android.wm.shell.Flags; 55 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; 56 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; 57 import com.android.wm.shell.common.ComponentUtils; 58 import com.android.wm.shell.shared.annotations.ShellBackgroundThread; 59 import com.android.wm.shell.shared.annotations.ShellMainThread; 60 import com.android.wm.shell.shared.bubbles.BubbleInfo; 61 import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; 62 import com.android.wm.shell.taskview.TaskView; 63 64 import java.io.PrintWriter; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.concurrent.Executor; 68 69 /** 70 * Encapsulates the data and UI elements of a bubble. 71 */ 72 public class Bubble implements BubbleViewProvider { 73 private static final String TAG = "Bubble"; 74 75 /** A string prefix used in app bubbles' {@link #mKey}. */ 76 public static final String KEY_APP_BUBBLE = "key_app_bubble"; 77 78 /** A string prefix used in note bubbles' {@link #mKey}. */ 79 public static final String KEY_NOTE_BUBBLE = "key_note_bubble"; 80 81 /** The possible types a bubble may be. */ 82 public enum BubbleType { 83 /** Chat is from a notification. */ 84 TYPE_CHAT, 85 /** Notes are from the note taking API. */ 86 TYPE_NOTE, 87 /** Shortcuts from bubble anything, based on {@link ShortcutInfo}. */ 88 TYPE_SHORTCUT, 89 /** Apps are from bubble anything. */ 90 TYPE_APP, 91 } 92 93 private final BubbleType mType; 94 95 private final String mKey; 96 @Nullable 97 private final String mGroupKey; 98 @Nullable 99 private final LocusId mLocusId; 100 101 private final Executor mMainExecutor; 102 private final Executor mBgExecutor; 103 104 private long mLastUpdated; 105 private long mLastAccessed; 106 107 @Nullable 108 private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; 109 110 /** Whether the bubble should show a dot for the notification indicating updated content. */ 111 private boolean mShowBubbleUpdateDot = true; 112 113 /** Whether flyout text should be suppressed, regardless of any other flags or state. */ 114 private boolean mSuppressFlyout; 115 116 // Items that are typically loaded later 117 private String mAppName; 118 private ShortcutInfo mShortcutInfo; 119 private String mMetadataShortcutId; 120 121 /** 122 * If {@link BubbleController#isShowingAsBubbleBar()} is true, the only view that will be 123 * populated will be {@link #mBubbleBarExpandedView}. If it is false, {@link #mIconView} 124 * and {@link #mExpandedView} will be populated. 125 */ 126 @Nullable 127 private BadgedImageView mIconView; 128 @Nullable 129 private BubbleExpandedView mExpandedView; 130 @Nullable 131 private BubbleBarExpandedView mBubbleBarExpandedView; 132 @Nullable 133 private BubbleTaskView mBubbleTaskView; 134 135 @Nullable 136 private BubbleViewInfoTask mInflationTask; 137 @Nullable 138 private BubbleViewInfoTaskLegacy mInflationTaskLegacy; 139 private boolean mInflateSynchronously; 140 private boolean mPendingIntentCanceled; 141 private boolean mIsImportantConversation; 142 143 /** 144 * Presentational info about the flyout. 145 */ 146 public static class FlyoutMessage { 147 @Nullable public Icon senderIcon; 148 @Nullable public Drawable senderAvatar; 149 @Nullable public CharSequence senderName; 150 @Nullable public CharSequence message; 151 @Nullable public boolean isGroupChat; 152 } 153 154 private FlyoutMessage mFlyoutMessage; 155 // The developer provided image for the bubble 156 private Bitmap mBubbleBitmap; 157 // The app badge for the bubble 158 private Bitmap mBadgeBitmap; 159 // App badge without any markings for important conversations 160 private Bitmap mRawBadgeBitmap; 161 private int mDotColor; 162 private Path mDotPath; 163 private int mFlags; 164 165 @NonNull 166 private UserHandle mUser; 167 @NonNull 168 private String mPackageName; 169 @Nullable 170 private String mTitle; 171 @Nullable 172 private Icon mIcon; 173 private boolean mIsBubble; 174 private boolean mIsTextChanged; 175 private boolean mIsDismissable; 176 private boolean mShouldSuppressNotificationDot; 177 private boolean mShouldSuppressNotificationList; 178 private boolean mShouldSuppressPeek; 179 private int mDesiredHeight; 180 @DimenRes 181 private int mDesiredHeightResId; 182 private int mTaskId; 183 184 /** for logging **/ 185 @Nullable 186 private InstanceId mInstanceId; 187 @Nullable 188 private String mChannelId; 189 private int mNotificationId; 190 private int mAppUid = -1; 191 192 /** 193 * A bubble is created and can be updated. This intent is updated until the user first 194 * expands the bubble. Once the user has expanded the contents, we ignore the intent updates 195 * to prevent restarting the intent & possibly altering UI state in the activity in front of 196 * the user. 197 * 198 * Once the bubble is overflowed, the activity is finished and updates to the 199 * notification are respected. Typically an update to an overflowed bubble would result in 200 * that bubble being added back to the stack anyways. 201 */ 202 @Nullable 203 private PendingIntent mPendingIntent; 204 private boolean mPendingIntentActive; 205 @Nullable 206 private PendingIntent.CancelListener mPendingIntentCancelListener; 207 208 /** 209 * Sent when the bubble & notification are no longer visible to the user (i.e. no 210 * notification in the shade, no bubble in the stack or overflow). 211 */ 212 @Nullable 213 private PendingIntent mDeleteIntent; 214 215 /** 216 * Used for app & note bubbles. 217 */ 218 @Nullable 219 private Intent mIntent; 220 221 /** 222 * Set while preparing a transition for animation. Several steps are needed before animation 223 * starts, so this is used to detect and route associated events to the coordinating transition. 224 */ 225 @Nullable 226 private BubbleTransitions.BubbleTransition mPreparingTransition; 227 228 /** 229 * Create a bubble with limited information based on given {@link ShortcutInfo}. 230 * Note: Currently this is only being used when the bubble is persisted to disk. 231 */ Bubble(@onNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, int taskId, @Nullable final String locus, boolean isDismissable, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor, final Bubbles.BubbleMetadataFlagListener listener)232 public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, 233 final int desiredHeight, final int desiredHeightResId, @Nullable final String title, 234 int taskId, @Nullable final String locus, boolean isDismissable, 235 @ShellMainThread Executor mainExecutor, 236 @ShellBackgroundThread Executor bgExecutor, 237 final Bubbles.BubbleMetadataFlagListener listener) { 238 Objects.requireNonNull(key); 239 Objects.requireNonNull(shortcutInfo); 240 mMetadataShortcutId = shortcutInfo.getId(); 241 mShortcutInfo = shortcutInfo; 242 mKey = key; 243 mGroupKey = null; 244 mLocusId = locus != null ? new LocusId(locus) : null; 245 mIsDismissable = isDismissable; 246 mFlags = 0; 247 mUser = shortcutInfo.getUserHandle(); 248 mPackageName = shortcutInfo.getPackage(); 249 mIcon = shortcutInfo.getIcon(); 250 mDesiredHeight = desiredHeight; 251 mDesiredHeightResId = desiredHeightResId; 252 mTitle = title; 253 mShowBubbleUpdateDot = false; 254 mMainExecutor = mainExecutor; 255 mBgExecutor = bgExecutor; 256 mTaskId = taskId; 257 mBubbleMetadataFlagListener = listener; 258 // TODO (b/394085999) read/write type to xml 259 mType = BubbleType.TYPE_CHAT; 260 } 261 Bubble( Intent intent, UserHandle user, @Nullable Icon icon, BubbleType type, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)262 private Bubble( 263 Intent intent, 264 UserHandle user, 265 @Nullable Icon icon, 266 BubbleType type, 267 String key, 268 @ShellMainThread Executor mainExecutor, 269 @ShellBackgroundThread Executor bgExecutor) { 270 mGroupKey = null; 271 mLocusId = null; 272 mFlags = 0; 273 mUser = user; 274 mIcon = icon; 275 mType = type; 276 mKey = key; 277 mShowBubbleUpdateDot = false; 278 mMainExecutor = mainExecutor; 279 mBgExecutor = bgExecutor; 280 mTaskId = INVALID_TASK_ID; 281 mIntent = intent; 282 mDesiredHeight = Integer.MAX_VALUE; 283 mPackageName = intent.getPackage(); 284 } 285 Bubble( PendingIntent intent, UserHandle user, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)286 private Bubble( 287 PendingIntent intent, 288 UserHandle user, 289 String key, 290 @ShellMainThread Executor mainExecutor, 291 @ShellBackgroundThread Executor bgExecutor) { 292 mGroupKey = null; 293 mLocusId = null; 294 mFlags = 0; 295 mUser = user; 296 mIcon = null; 297 mType = BubbleType.TYPE_APP; 298 mKey = key; 299 mShowBubbleUpdateDot = false; 300 mMainExecutor = mainExecutor; 301 mBgExecutor = bgExecutor; 302 mTaskId = INVALID_TASK_ID; 303 mPendingIntent = intent; 304 mIntent = null; 305 mDesiredHeight = Integer.MAX_VALUE; 306 mPackageName = ComponentUtils.getPackageName(intent); 307 } 308 Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)309 private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, 310 @ShellBackgroundThread Executor bgExecutor) { 311 mGroupKey = null; 312 mLocusId = null; 313 mFlags = 0; 314 mUser = info.getUserHandle(); 315 mIcon = info.getIcon(); 316 mType = BubbleType.TYPE_SHORTCUT; 317 mKey = getBubbleKeyForShortcut(info); 318 mShowBubbleUpdateDot = false; 319 mMainExecutor = mainExecutor; 320 mBgExecutor = bgExecutor; 321 mTaskId = INVALID_TASK_ID; 322 mIntent = null; 323 mDesiredHeight = Integer.MAX_VALUE; 324 mPackageName = info.getPackage(); 325 mShortcutInfo = info; 326 } 327 Bubble( TaskInfo task, UserHandle user, @Nullable Icon icon, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)328 private Bubble( 329 TaskInfo task, 330 UserHandle user, 331 @Nullable Icon icon, 332 String key, 333 @ShellMainThread Executor mainExecutor, 334 @ShellBackgroundThread Executor bgExecutor) { 335 mGroupKey = null; 336 mLocusId = null; 337 mFlags = 0; 338 mUser = user; 339 mIcon = icon; 340 mType = BubbleType.TYPE_APP; 341 mKey = key; 342 mShowBubbleUpdateDot = false; 343 mMainExecutor = mainExecutor; 344 mBgExecutor = bgExecutor; 345 mTaskId = task.taskId; 346 mIntent = null; 347 mDesiredHeight = Integer.MAX_VALUE; 348 mPackageName = task.baseActivity.getPackageName(); 349 } 350 351 /** Creates a note taking bubble. */ createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)352 public static Bubble createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon, 353 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 354 return new Bubble(intent, 355 user, 356 icon, 357 BubbleType.TYPE_NOTE, 358 getNoteBubbleKeyForApp(intent.getPackage(), user), 359 mainExecutor, bgExecutor); 360 } 361 362 /** Creates an app bubble. */ createAppBubble(PendingIntent intent, UserHandle user, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)363 public static Bubble createAppBubble(PendingIntent intent, UserHandle user, 364 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 365 return new Bubble(intent, 366 user, 367 /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), 368 mainExecutor, bgExecutor); 369 } 370 371 /** Creates an app bubble. */ createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)372 public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, 373 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 374 return new Bubble(intent, 375 user, 376 icon, 377 BubbleType.TYPE_APP, 378 getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), 379 mainExecutor, bgExecutor); 380 } 381 382 /** Creates a task bubble. */ createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)383 public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, 384 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 385 return new Bubble(info, 386 user, 387 icon, 388 getAppBubbleKeyForTask(info), 389 mainExecutor, bgExecutor); 390 } 391 392 /** Creates a shortcut bubble. */ createShortcutBubble( ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)393 public static Bubble createShortcutBubble( 394 ShortcutInfo info, 395 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 396 return new Bubble(info, mainExecutor, bgExecutor); 397 } 398 399 /** 400 * Returns the key for an app bubble from an app with package name, {@code packageName} on an 401 * Android user, {@code user}. 402 */ getAppBubbleKeyForApp(String packageName, UserHandle user)403 public static String getAppBubbleKeyForApp(String packageName, UserHandle user) { 404 Objects.requireNonNull(packageName); 405 Objects.requireNonNull(user); 406 return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; 407 } 408 409 /** 410 * Returns the key for a note bubble from an app with package name, {@code packageName} on an 411 * Android user, {@code user}. 412 */ getNoteBubbleKeyForApp(String packageName, UserHandle user)413 public static String getNoteBubbleKeyForApp(String packageName, UserHandle user) { 414 Objects.requireNonNull(packageName); 415 Objects.requireNonNull(user); 416 return KEY_NOTE_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; 417 } 418 419 /** 420 * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the 421 * {@code shortcutInfo} id. 422 */ getBubbleKeyForShortcut(ShortcutInfo info)423 public static String getBubbleKeyForShortcut(ShortcutInfo info) { 424 return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); 425 } 426 427 /** 428 * Returns the key for an app bubble from an app with package name, {@code packageName} on an 429 * Android user, {@code user}. 430 */ getAppBubbleKeyForTask(TaskInfo taskInfo)431 public static String getAppBubbleKeyForTask(TaskInfo taskInfo) { 432 Objects.requireNonNull(taskInfo); 433 return KEY_APP_BUBBLE + ":" + taskInfo.taskId; 434 } 435 436 /** 437 * Creates a chat bubble based on a notification (contents of {@link BubbleEntry}. 438 */ 439 @VisibleForTesting(visibility = PRIVATE) Bubble(@onNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)440 public Bubble(@NonNull final BubbleEntry entry, 441 final Bubbles.BubbleMetadataFlagListener listener, 442 final Bubbles.PendingIntentCanceledListener intentCancelListener, 443 @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { 444 mType = BubbleType.TYPE_CHAT; 445 mKey = entry.getKey(); 446 mGroupKey = entry.getGroupKey(); 447 mLocusId = entry.getLocusId(); 448 mBubbleMetadataFlagListener = listener; 449 mPendingIntentCancelListener = intent -> { 450 if (mPendingIntent != null) { 451 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); 452 } 453 mainExecutor.execute(() -> { 454 intentCancelListener.onPendingIntentCanceled(this); 455 }); 456 }; 457 mMainExecutor = mainExecutor; 458 mBgExecutor = bgExecutor; 459 mTaskId = INVALID_TASK_ID; 460 setEntry(entry); 461 } 462 463 /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */ asBubbleBarBubble()464 public BubbleInfo asBubbleBarBubble() { 465 return new BubbleInfo(getKey(), 466 getFlags(), 467 getShortcutId(), 468 getIcon(), 469 getUser().getIdentifier(), 470 getPackageName(), 471 getTitle(), 472 getAppName(), 473 isImportantConversation(), 474 showAppBadge(), 475 getParcelableFlyoutMessage()); 476 } 477 478 /** Creates a parcelable flyout message to send to launcher. */ 479 @Nullable getParcelableFlyoutMessage()480 private ParcelableFlyoutMessage getParcelableFlyoutMessage() { 481 if (mFlyoutMessage == null) { 482 return null; 483 } 484 // the icon is only used in group chats 485 Icon icon = mFlyoutMessage.isGroupChat ? mFlyoutMessage.senderIcon : null; 486 String title = 487 mFlyoutMessage.senderName == null ? null : mFlyoutMessage.senderName.toString(); 488 String message = mFlyoutMessage.message == null ? null : mFlyoutMessage.message.toString(); 489 return new ParcelableFlyoutMessage(icon, title, message); 490 } 491 492 @Override getKey()493 public String getKey() { 494 return mKey; 495 } 496 497 @Hide isDismissable()498 public boolean isDismissable() { 499 return mIsDismissable; 500 } 501 502 /** 503 * @see StatusBarNotification#getGroupKey() 504 * @return the group key for this bubble, if one exists. 505 */ getGroupKey()506 public String getGroupKey() { 507 return mGroupKey; 508 } 509 getLocusId()510 public LocusId getLocusId() { 511 return mLocusId; 512 } 513 getUser()514 public UserHandle getUser() { 515 return mUser; 516 } 517 518 @NonNull getPackageName()519 public String getPackageName() { 520 return mPackageName; 521 } 522 523 @Override getBubbleIcon()524 public Bitmap getBubbleIcon() { 525 return mBubbleBitmap; 526 } 527 528 @Override getAppBadge()529 public Bitmap getAppBadge() { 530 return mBadgeBitmap; 531 } 532 533 @Override getRawAppBadge()534 public Bitmap getRawAppBadge() { 535 return mRawBadgeBitmap; 536 } 537 538 @Override getDotColor()539 public int getDotColor() { 540 return mDotColor; 541 } 542 543 @Override getDotPath()544 public Path getDotPath() { 545 return mDotPath; 546 } 547 548 @Nullable getAppName()549 public String getAppName() { 550 return mAppName; 551 } 552 553 @Nullable getShortcutInfo()554 public ShortcutInfo getShortcutInfo() { 555 return mShortcutInfo; 556 } 557 558 @Nullable 559 @Override getIconView()560 public BadgedImageView getIconView() { 561 return mIconView; 562 } 563 564 @Nullable 565 @Override getExpandedView()566 public BubbleExpandedView getExpandedView() { 567 return mExpandedView; 568 } 569 570 @Nullable 571 @Override getBubbleBarExpandedView()572 public BubbleBarExpandedView getBubbleBarExpandedView() { 573 return mBubbleBarExpandedView; 574 } 575 576 @Nullable getTitle()577 public String getTitle() { 578 return mTitle; 579 } 580 581 /** 582 * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new 583 * instance of {@link BubbleTaskView} is created. 584 */ getOrCreateBubbleTaskView(BubbleTaskViewFactory taskViewFactory)585 public BubbleTaskView getOrCreateBubbleTaskView(BubbleTaskViewFactory taskViewFactory) { 586 if (mBubbleTaskView == null) { 587 mBubbleTaskView = taskViewFactory.create(); 588 } 589 return mBubbleTaskView; 590 } 591 getTaskView()592 public TaskView getTaskView() { 593 return mBubbleTaskView.getTaskView(); 594 } 595 596 /** 597 * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. 598 */ getShortcutId()599 String getShortcutId() { 600 return getShortcutInfo() != null 601 ? getShortcutInfo().getId() 602 : getMetadataShortcutId(); 603 } 604 getMetadataShortcutId()605 String getMetadataShortcutId() { 606 return mMetadataShortcutId; 607 } 608 hasMetadataShortcutId()609 boolean hasMetadataShortcutId() { 610 return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); 611 } 612 getPreparingTransition()613 public BubbleTransitions.BubbleTransition getPreparingTransition() { 614 return mPreparingTransition; 615 } 616 617 /** 618 * Call this to clean up the task for the bubble. Ensure this is always called when done with 619 * the bubble. 620 */ cleanupExpandedView()621 void cleanupExpandedView() { 622 cleanupExpandedView(true); 623 } 624 cleanupExpandedView(boolean cleanupTaskView)625 private void cleanupExpandedView(boolean cleanupTaskView) { 626 if (mExpandedView != null) { 627 mExpandedView.cleanUpExpandedState(); 628 mExpandedView = null; 629 } 630 if (mBubbleBarExpandedView != null) { 631 mBubbleBarExpandedView.cleanUpExpandedState(); 632 mBubbleBarExpandedView = null; 633 } 634 if (cleanupTaskView) { 635 cleanupTaskView(); 636 } 637 if (mPendingIntent != null) { 638 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); 639 } 640 mPendingIntentActive = false; 641 } 642 643 /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */ cleanupTaskView()644 public void cleanupTaskView() { 645 if (mBubbleTaskView != null) { 646 mBubbleTaskView.cleanup(); 647 mBubbleTaskView = null; 648 } 649 } 650 651 /** 652 * Call when all the views should be removed/cleaned up. 653 */ cleanupViews()654 public void cleanupViews() { 655 ProtoLog.d(WM_SHELL_BUBBLES, "Bubble#cleanupViews=%s", getKey()); 656 cleanupViews(true); 657 } 658 659 /** 660 * Call when all the views should be removed/cleaned up. 661 * 662 * <p>If we're switching between bar and floating modes, pass {@code false} on 663 * {@code cleanupTaskView} to avoid recreating it in the new mode. 664 */ cleanupViews(boolean cleanupTaskView)665 public void cleanupViews(boolean cleanupTaskView) { 666 cleanupExpandedView(cleanupTaskView); 667 mIconView = null; 668 } 669 setPendingIntentCanceled()670 void setPendingIntentCanceled() { 671 mPendingIntentCanceled = true; 672 } 673 getPendingIntentCanceled()674 boolean getPendingIntentCanceled() { 675 return mPendingIntentCanceled; 676 } 677 678 /** 679 * Sets whether to perform inflation on the same thread as the caller. This method should only 680 * be used in tests, not in production. 681 */ 682 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)683 void setInflateSynchronously(boolean inflateSynchronously) { 684 mInflateSynchronously = inflateSynchronously; 685 } 686 687 /** 688 * Sets the current bubble-transition that is coordinating a change in this bubble. 689 */ setPreparingTransition(BubbleTransitions.BubbleTransition transit)690 void setPreparingTransition(BubbleTransitions.BubbleTransition transit) { 691 mPreparingTransition = transit; 692 } 693 694 /** 695 * Sets whether this bubble is considered text changed. This method is purely for 696 * testing. 697 */ 698 @VisibleForTesting setTextChangedForTest(boolean textChanged)699 void setTextChangedForTest(boolean textChanged) { 700 mIsTextChanged = textChanged; 701 } 702 703 /** 704 * Starts a task to inflate & load any necessary information to display a bubble. 705 * 706 * @param callback the callback to notify one the bubble is ready to be displayed. 707 * @param context the context for the bubble. 708 * @param expandedViewManager the bubble expanded view manager. 709 * @param taskViewFactory the task view factory used to create the task view for the bubble. 710 * @param positioner the bubble positioner. 711 * @param stackView the view the bubble is added to, iff showing as floating. 712 * @param layerView the layer the bubble is added to, iff showing in the bubble bar. 713 * @param iconFactory the icon factory used to create images for the bubble. 714 */ inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean skipInflation)715 void inflate(BubbleViewInfoTask.Callback callback, 716 Context context, 717 BubbleExpandedViewManager expandedViewManager, 718 BubbleTaskViewFactory taskViewFactory, 719 BubblePositioner positioner, 720 @Nullable BubbleStackView stackView, 721 @Nullable BubbleBarLayerView layerView, 722 BubbleIconFactory iconFactory, 723 boolean skipInflation) { 724 ProtoLog.v(WM_SHELL_BUBBLES, "Inflate bubble key=%s", getKey()); 725 if (Flags.bubbleViewInfoExecutors()) { 726 if (mInflationTask != null && !mInflationTask.isFinished()) { 727 mInflationTask.cancel(); 728 } 729 mInflationTask = new BubbleViewInfoTask(this, 730 context, 731 expandedViewManager, 732 taskViewFactory, 733 positioner, 734 stackView, 735 layerView, 736 iconFactory, 737 skipInflation, 738 callback, 739 mMainExecutor, 740 mBgExecutor); 741 if (mInflateSynchronously) { 742 mInflationTask.startSync(); 743 } else { 744 mInflationTask.start(); 745 } 746 } else { 747 if (mInflationTaskLegacy != null && mInflationTaskLegacy.getStatus() != FINISHED) { 748 mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */); 749 } 750 mInflationTaskLegacy = new BubbleViewInfoTaskLegacy(this, 751 context, 752 expandedViewManager, 753 taskViewFactory, 754 positioner, 755 stackView, 756 layerView, 757 iconFactory, 758 skipInflation, 759 bubble -> { 760 if (callback != null) { 761 callback.onBubbleViewsReady(bubble); 762 } 763 }, 764 mMainExecutor, 765 mBgExecutor); 766 if (mInflateSynchronously) { 767 mInflationTaskLegacy.onPostExecute(mInflationTaskLegacy.doInBackground()); 768 } else { 769 mInflationTaskLegacy.execute(); 770 } 771 } 772 } 773 isInflated()774 boolean isInflated() { 775 return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null; 776 } 777 stopInflation()778 void stopInflation() { 779 if (Flags.bubbleViewInfoExecutors()) { 780 if (mInflationTask == null) { 781 return; 782 } 783 mInflationTask.cancel(); 784 } else { 785 if (mInflationTaskLegacy == null) { 786 return; 787 } 788 mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */); 789 } 790 } 791 setViewInfo(BubbleViewInfoTask.BubbleViewInfo info)792 void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { 793 if (!isInflated()) { 794 mIconView = info.imageView; 795 mExpandedView = info.expandedView; 796 mBubbleBarExpandedView = info.bubbleBarExpandedView; 797 } 798 799 mShortcutInfo = info.shortcutInfo; 800 mAppName = info.appName; 801 if (mTitle == null) { 802 mTitle = mAppName; 803 } 804 mFlyoutMessage = info.flyoutMessage; 805 806 mBadgeBitmap = info.badgeBitmap; 807 mRawBadgeBitmap = info.rawBadgeBitmap; 808 mBubbleBitmap = info.bubbleBitmap; 809 810 mDotColor = info.dotColor; 811 mDotPath = info.dotPath; 812 813 if (mExpandedView != null) { 814 mExpandedView.update(this /* bubble */); 815 } 816 if (mBubbleBarExpandedView != null) { 817 mBubbleBarExpandedView.update(this /* bubble */); 818 } 819 if (mIconView != null) { 820 mIconView.setRenderedBubble(this /* bubble */); 821 } 822 } 823 824 /** 825 * @deprecated {@link BubbleViewInfoTaskLegacy} is deprecated. 826 */ 827 @Deprecated setViewInfoLegacy(BubbleViewInfoTaskLegacy.BubbleViewInfo info)828 void setViewInfoLegacy(BubbleViewInfoTaskLegacy.BubbleViewInfo info) { 829 if (!isInflated()) { 830 mIconView = info.imageView; 831 mExpandedView = info.expandedView; 832 mBubbleBarExpandedView = info.bubbleBarExpandedView; 833 } 834 835 mShortcutInfo = info.shortcutInfo; 836 mAppName = info.appName; 837 if (mTitle == null) { 838 mTitle = mAppName; 839 } 840 mFlyoutMessage = info.flyoutMessage; 841 842 mBadgeBitmap = info.badgeBitmap; 843 mRawBadgeBitmap = info.rawBadgeBitmap; 844 mBubbleBitmap = info.bubbleBitmap; 845 846 mDotColor = info.dotColor; 847 mDotPath = info.dotPath; 848 849 if (mExpandedView != null) { 850 mExpandedView.update(this /* bubble */); 851 } 852 if (mBubbleBarExpandedView != null) { 853 mBubbleBarExpandedView.update(this /* bubble */); 854 } 855 if (mIconView != null) { 856 mIconView.setRenderedBubble(this /* bubble */); 857 } 858 } 859 860 /** 861 * Set visibility of bubble in the expanded state. 862 * 863 * <p>Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 864 * and setting {@code false} actually means rendering the expanded view in transparent. 865 * 866 * @param visibility {@code true} if the expanded bubble should be visible on the screen. 867 */ 868 @Override setTaskViewVisibility(boolean visibility)869 public void setTaskViewVisibility(boolean visibility) { 870 if (mExpandedView != null) { 871 mExpandedView.setContentVisibility(visibility); 872 } 873 } 874 875 /** 876 * Sets the entry associated with this bubble. 877 */ setEntry(@onNull final BubbleEntry entry)878 void setEntry(@NonNull final BubbleEntry entry) { 879 Objects.requireNonNull(entry); 880 boolean showingDotPreviously = showDot(); 881 mLastUpdated = entry.getStatusBarNotification().getPostTime(); 882 mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); 883 mPackageName = entry.getStatusBarNotification().getPackageName(); 884 mUser = entry.getStatusBarNotification().getUser(); 885 mTitle = getTitle(entry); 886 mChannelId = entry.getStatusBarNotification().getNotification().getChannelId(); 887 mNotificationId = entry.getStatusBarNotification().getId(); 888 mAppUid = entry.getStatusBarNotification().getUid(); 889 mInstanceId = entry.getStatusBarNotification().getInstanceId(); 890 mFlyoutMessage = extractFlyoutMessage(entry); 891 if (entry.getRanking() != null) { 892 mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); 893 mIsTextChanged = entry.getRanking().isTextChanged(); 894 if (entry.getRanking().getChannel() != null) { 895 mIsImportantConversation = 896 entry.getRanking().getChannel().isImportantConversation(); 897 } 898 } 899 if (entry.getBubbleMetadata() != null) { 900 mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId(); 901 mFlags = entry.getBubbleMetadata().getFlags(); 902 mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); 903 mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); 904 mIcon = entry.getBubbleMetadata().getIcon(); 905 906 if (!mPendingIntentActive || mPendingIntent == null) { 907 if (mPendingIntent != null) { 908 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); 909 } 910 mPendingIntent = entry.getBubbleMetadata().getIntent(); 911 if (mPendingIntent != null) { 912 mPendingIntent.registerCancelListener(mPendingIntentCancelListener); 913 } 914 } else if (mPendingIntent != null && entry.getBubbleMetadata().getIntent() == null) { 915 // Was an intent bubble now it's a shortcut bubble... still unregister the listener 916 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); 917 mPendingIntentActive = false; 918 mPendingIntent = null; 919 } 920 mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); 921 } 922 923 mIsDismissable = entry.isDismissable(); 924 mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); 925 mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); 926 mShouldSuppressPeek = entry.shouldSuppressPeek(); 927 if (showingDotPreviously != showDot()) { 928 // This will update the UI if needed 929 setShowDot(showDot()); 930 } 931 } 932 933 /** 934 * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles 935 * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the 936 * icon from the shortcut. 937 */ 938 @Nullable getIcon()939 public Icon getIcon() { 940 return mIcon; 941 } 942 isTextChanged()943 boolean isTextChanged() { 944 return mIsTextChanged; 945 } 946 947 /** 948 * @return the last time this bubble was updated or accessed, whichever is most recent. 949 */ getLastActivity()950 long getLastActivity() { 951 return Math.max(mLastUpdated, mLastAccessed); 952 } 953 954 /** 955 * Sets if the intent used for this bubble is currently active (i.e. populating an 956 * expanded view, expanded or not). 957 */ setPendingIntentActive()958 void setPendingIntentActive() { 959 mPendingIntentActive = true; 960 } 961 962 /** 963 * Whether the pending intent of this bubble is active (i.e. has been sent). 964 */ isPendingIntentActive()965 boolean isPendingIntentActive() { 966 return mPendingIntentActive; 967 } 968 getInstanceId()969 public InstanceId getInstanceId() { 970 return mInstanceId; 971 } 972 973 @Nullable getChannelId()974 public String getChannelId() { 975 return mChannelId; 976 } 977 getNotificationId()978 public int getNotificationId() { 979 return mNotificationId; 980 } 981 982 /** 983 * @return the task id of the task in which bubble contents is drawn. 984 */ 985 @Override getTaskId()986 public int getTaskId() { 987 if (mBubbleBarExpandedView != null) { 988 return mBubbleBarExpandedView.getTaskId(); 989 } 990 return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId; 991 } 992 993 /** 994 * Should be invoked whenever a Bubble is accessed (selected while expanded). 995 */ markAsAccessedAt(long lastAccessedMillis)996 void markAsAccessedAt(long lastAccessedMillis) { 997 mLastAccessed = lastAccessedMillis; 998 setSuppressNotification(true); 999 setShowDot(false /* show */); 1000 } 1001 1002 /** 1003 * Should be invoked whenever a Bubble is promoted from overflow. 1004 */ markUpdatedAt(long lastAccessedMillis)1005 void markUpdatedAt(long lastAccessedMillis) { 1006 mLastUpdated = lastAccessedMillis; 1007 } 1008 1009 /** 1010 * Whether this notification should be shown in the shade. 1011 */ showInShade()1012 boolean showInShade() { 1013 return !shouldSuppressNotification() || !mIsDismissable; 1014 } 1015 1016 /** 1017 * Whether this bubble is currently being hidden from the stack. 1018 */ isSuppressed()1019 boolean isSuppressed() { 1020 return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0; 1021 } 1022 1023 /** 1024 * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API to 1025 * hide the bubble when in the same content). 1026 */ isSuppressable()1027 boolean isSuppressable() { 1028 return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0; 1029 } 1030 1031 /** 1032 * Whether this notification conversation is important. 1033 */ isImportantConversation()1034 boolean isImportantConversation() { 1035 return mIsImportantConversation; 1036 } 1037 1038 /** 1039 * Sets whether this notification should be suppressed in the shade. 1040 */ 1041 @VisibleForTesting setSuppressNotification(boolean suppressNotification)1042 public void setSuppressNotification(boolean suppressNotification) { 1043 boolean prevShowInShade = showInShade(); 1044 if (suppressNotification) { 1045 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1046 } else { 1047 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1048 } 1049 1050 if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) { 1051 mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); 1052 } 1053 } 1054 1055 /** 1056 * Sets whether this bubble should be suppressed from the stack. 1057 */ setSuppressBubble(boolean suppressBubble)1058 public void setSuppressBubble(boolean suppressBubble) { 1059 if (!isSuppressable()) { 1060 Log.e(TAG, "calling setSuppressBubble on " 1061 + getKey() + " when bubble not suppressable"); 1062 return; 1063 } 1064 boolean prevSuppressed = isSuppressed(); 1065 if (suppressBubble) { 1066 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 1067 } else { 1068 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 1069 } 1070 if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) { 1071 mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); 1072 } 1073 } 1074 1075 /** 1076 * Sets whether the bubble for this notification should show a dot indicating updated content. 1077 */ setShowDot(boolean showDot)1078 void setShowDot(boolean showDot) { 1079 mShowBubbleUpdateDot = showDot; 1080 1081 if (mIconView != null) { 1082 mIconView.updateDotVisibility(true /* animate */); 1083 } 1084 } 1085 1086 /** 1087 * Whether the bubble for this notification should show a dot indicating updated content. 1088 */ 1089 @Override showDot()1090 public boolean showDot() { 1091 return mShowBubbleUpdateDot 1092 && !mShouldSuppressNotificationDot 1093 && !shouldSuppressNotification(); 1094 } 1095 1096 /** 1097 * Whether the flyout for the bubble should be shown. 1098 */ 1099 @VisibleForTesting showFlyout()1100 public boolean showFlyout() { 1101 return !mSuppressFlyout && !mShouldSuppressPeek 1102 && !shouldSuppressNotification() 1103 && !mShouldSuppressNotificationList; 1104 } 1105 1106 /** 1107 * Set whether the flyout text for the bubble should be shown when an update is received. 1108 * 1109 * @param suppressFlyout whether the flyout text is shown 1110 */ setSuppressFlyout(boolean suppressFlyout)1111 void setSuppressFlyout(boolean suppressFlyout) { 1112 mSuppressFlyout = suppressFlyout; 1113 } 1114 getFlyoutMessage()1115 FlyoutMessage getFlyoutMessage() { 1116 return mFlyoutMessage; 1117 } 1118 getRawDesiredHeight()1119 int getRawDesiredHeight() { 1120 return mDesiredHeight; 1121 } 1122 getRawDesiredHeightResId()1123 int getRawDesiredHeightResId() { 1124 return mDesiredHeightResId; 1125 } 1126 getDesiredHeight(Context context)1127 float getDesiredHeight(Context context) { 1128 boolean useRes = mDesiredHeightResId != 0; 1129 if (useRes) { 1130 return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName, 1131 mUser.getIdentifier()); 1132 } else { 1133 return mDesiredHeight * context.getResources().getDisplayMetrics().density; 1134 } 1135 } 1136 getDesiredHeightString()1137 String getDesiredHeightString() { 1138 boolean useRes = mDesiredHeightResId != 0; 1139 if (useRes) { 1140 return String.valueOf(mDesiredHeightResId); 1141 } else { 1142 return String.valueOf(mDesiredHeight); 1143 } 1144 } 1145 1146 /** 1147 * Returns the pending intent used to populate the bubble. 1148 */ 1149 @Nullable getPendingIntent()1150 PendingIntent getPendingIntent() { 1151 return mPendingIntent; 1152 } 1153 1154 /** 1155 * Whether an app badge should be shown for this bubble. 1156 */ showAppBadge()1157 public boolean showAppBadge() { 1158 return isChat() || isShortcut() || isNote(); 1159 } 1160 1161 /** 1162 * Returns the pending intent to send when a bubble is dismissed (set via the notification API). 1163 */ 1164 @Nullable getDeleteIntent()1165 PendingIntent getDeleteIntent() { 1166 return mDeleteIntent; 1167 } 1168 1169 /** 1170 * Returns the intent used to populate the bubble. 1171 */ 1172 @Nullable getIntent()1173 public Intent getIntent() { 1174 return mIntent; 1175 } 1176 1177 /** 1178 * Sets the intent used to populate the bubble. 1179 */ setIntent(Intent intent)1180 void setIntent(Intent intent) { 1181 mIntent = intent; 1182 } 1183 1184 /** 1185 * Returns whether this bubble is a conversation from the notification API. 1186 */ isChat()1187 public boolean isChat() { 1188 return mType == BubbleType.TYPE_CHAT; 1189 } 1190 1191 /** 1192 * Returns whether this bubble is a note from the note taking API. 1193 */ isNote()1194 public boolean isNote() { 1195 return mType == BubbleType.TYPE_NOTE; 1196 } 1197 1198 /** 1199 * Returns whether this bubble is a shortcut. 1200 */ isShortcut()1201 public boolean isShortcut() { 1202 return mType == BubbleType.TYPE_SHORTCUT; 1203 } 1204 1205 /** 1206 * Returns whether this bubble is an app. 1207 */ isApp()1208 public boolean isApp() { 1209 return mType == BubbleType.TYPE_APP; 1210 } 1211 1212 /** Creates open app settings intent */ getSettingsIntent(final Context context)1213 public Intent getSettingsIntent(final Context context) { 1214 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); 1215 intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); 1216 final int uid = getUid(context); 1217 if (uid != -1) { 1218 intent.putExtra(Settings.EXTRA_APP_UID, uid); 1219 } 1220 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1221 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1222 return intent; 1223 } 1224 getAppUid()1225 public int getAppUid() { 1226 return mAppUid; 1227 } 1228 getUid(final Context context)1229 private int getUid(final Context context) { 1230 if (mAppUid != -1) return mAppUid; 1231 final PackageManager pm = BubbleController.getPackageManagerForUser(context, 1232 mUser.getIdentifier()); 1233 if (pm == null) return -1; 1234 try { 1235 final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0); 1236 return info.uid; 1237 } catch (PackageManager.NameNotFoundException e) { 1238 Log.e(TAG, "cannot find uid", e); 1239 } 1240 return -1; 1241 } 1242 getDimenForPackageUser(Context context, int resId, String pkg, int userId)1243 private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { 1244 Resources r; 1245 if (pkg != null) { 1246 try { 1247 if (userId == UserHandle.USER_ALL) { 1248 userId = UserHandle.USER_SYSTEM; 1249 } 1250 r = context.createContextAsUser(UserHandle.of(userId), /* flags */ 0) 1251 .getPackageManager().getResourcesForApplication(pkg); 1252 return r.getDimensionPixelSize(resId); 1253 } catch (PackageManager.NameNotFoundException ex) { 1254 // Uninstalled, don't care 1255 } catch (Resources.NotFoundException e) { 1256 // Invalid res id, return 0 and user our default 1257 Log.e(TAG, "Couldn't find desired height res id", e); 1258 } 1259 } 1260 return 0; 1261 } 1262 shouldSuppressNotification()1263 private boolean shouldSuppressNotification() { 1264 return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); 1265 } 1266 shouldAutoExpand()1267 public boolean shouldAutoExpand() { 1268 return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 1269 } 1270 1271 @VisibleForTesting setShouldAutoExpand(boolean shouldAutoExpand)1272 public void setShouldAutoExpand(boolean shouldAutoExpand) { 1273 boolean prevAutoExpand = shouldAutoExpand(); 1274 if (shouldAutoExpand) { 1275 enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 1276 } else { 1277 disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 1278 } 1279 if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) { 1280 mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); 1281 } 1282 } 1283 setIsBubble(final boolean isBubble)1284 public void setIsBubble(final boolean isBubble) { 1285 mIsBubble = isBubble; 1286 } 1287 isBubble()1288 public boolean isBubble() { 1289 return mIsBubble; 1290 } 1291 enable(int option)1292 public void enable(int option) { 1293 mFlags |= option; 1294 } 1295 disable(int option)1296 public void disable(int option) { 1297 mFlags &= ~option; 1298 } 1299 isEnabled(int option)1300 public boolean isEnabled(int option) { 1301 return (mFlags & option) != 0; 1302 } 1303 getFlags()1304 public int getFlags() { 1305 return mFlags; 1306 } 1307 1308 @Override toString()1309 public String toString() { 1310 return "Bubble{" + mKey + '}'; 1311 } 1312 1313 /** 1314 * Description of current bubble state. 1315 */ dump(@onNull PrintWriter pw)1316 public void dump(@NonNull PrintWriter pw) { 1317 pw.print("key: "); pw.println(mKey); 1318 pw.print(" showInShade: "); pw.println(showInShade()); 1319 pw.print(" showDot: "); pw.println(showDot()); 1320 pw.print(" showFlyout: "); pw.println(showFlyout()); 1321 pw.print(" lastActivity: "); pw.println(getLastActivity()); 1322 pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); 1323 pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); 1324 pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); 1325 pw.print(" isDismissable: "); pw.println(mIsDismissable); 1326 pw.println(" bubbleMetadataFlagListener null?: " + (mBubbleMetadataFlagListener == null)); 1327 if (mExpandedView != null) { 1328 mExpandedView.dump(pw, " "); 1329 } 1330 } 1331 1332 @Override equals(Object o)1333 public boolean equals(Object o) { 1334 if (this == o) return true; 1335 if (!(o instanceof Bubble)) return false; 1336 Bubble bubble = (Bubble) o; 1337 return Objects.equals(mKey, bubble.mKey); 1338 } 1339 1340 @Override hashCode()1341 public int hashCode() { 1342 return Objects.hash(mKey); 1343 } 1344 1345 @Nullable getTitle(@onNull final BubbleEntry e)1346 private static String getTitle(@NonNull final BubbleEntry e) { 1347 final CharSequence titleCharSeq = e.getStatusBarNotification() 1348 .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); 1349 return titleCharSeq == null ? null : titleCharSeq.toString(); 1350 } 1351 1352 /** 1353 * Returns our best guess for the most relevant text summary of the latest update to this 1354 * notification, based on its type. Returns null if there should not be an update message. 1355 */ 1356 @NonNull extractFlyoutMessage(BubbleEntry entry)1357 static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) { 1358 Objects.requireNonNull(entry); 1359 final Notification underlyingNotif = entry.getStatusBarNotification().getNotification(); 1360 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); 1361 1362 Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); 1363 bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( 1364 Notification.EXTRA_IS_GROUP_CONVERSATION); 1365 try { 1366 if (Notification.BigTextStyle.class.equals(style)) { 1367 // Return the big text, it is big so probably important. If it's not there use the 1368 // normal text. 1369 CharSequence bigText = 1370 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); 1371 bubbleMessage.message = !TextUtils.isEmpty(bigText) 1372 ? bigText 1373 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 1374 return bubbleMessage; 1375 } else if (Notification.MessagingStyle.class.equals(style)) { 1376 final List<Notification.MessagingStyle.Message> messages = 1377 Notification.MessagingStyle.Message.getMessagesFromBundleArray( 1378 (Parcelable[]) underlyingNotif.extras.get( 1379 Notification.EXTRA_MESSAGES)); 1380 1381 final Notification.MessagingStyle.Message latestMessage = 1382 Notification.MessagingStyle.findLatestIncomingMessage(messages); 1383 if (latestMessage != null) { 1384 bubbleMessage.message = latestMessage.getText(); 1385 Person sender = latestMessage.getSenderPerson(); 1386 bubbleMessage.senderName = sender != null ? sender.getName() : null; 1387 bubbleMessage.senderAvatar = null; 1388 bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; 1389 return bubbleMessage; 1390 } 1391 } else if (Notification.InboxStyle.class.equals(style)) { 1392 CharSequence[] lines = 1393 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); 1394 1395 // Return the last line since it should be the most recent. 1396 if (lines != null && lines.length > 0) { 1397 bubbleMessage.message = lines[lines.length - 1]; 1398 return bubbleMessage; 1399 } 1400 } else if (Notification.MediaStyle.class.equals(style)) { 1401 // Return nothing, media updates aren't typically useful as a text update. 1402 return bubbleMessage; 1403 } else { 1404 // Default to text extra. 1405 bubbleMessage.message = 1406 underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 1407 return bubbleMessage; 1408 } 1409 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { 1410 // No use crashing, we'll just return null and the caller will assume there's no update 1411 // message. 1412 e.printStackTrace(); 1413 } 1414 1415 return bubbleMessage; 1416 } 1417 } 1418