1 /* 2 * Copyright (C) 2019 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.systemui.bubbles; 17 18 import static android.os.AsyncTask.Status.FINISHED; 19 import static android.view.Display.INVALID_DISPLAY; 20 21 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 22 23 import android.annotation.DimenRes; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.Notification; 27 import android.app.PendingIntent; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ShortcutInfo; 33 import android.content.res.Resources; 34 import android.graphics.Bitmap; 35 import android.graphics.Path; 36 import android.graphics.drawable.Drawable; 37 import android.graphics.drawable.Icon; 38 import android.os.UserHandle; 39 import android.provider.Settings; 40 import android.util.Log; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.logging.InstanceId; 44 import com.android.systemui.shared.system.SysUiStatsLog; 45 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 46 import com.android.systemui.statusbar.phone.StatusBar; 47 48 import java.io.FileDescriptor; 49 import java.io.PrintWriter; 50 import java.util.Objects; 51 52 /** 53 * Encapsulates the data and UI elements of a bubble. 54 */ 55 class Bubble implements BubbleViewProvider { 56 private static final String TAG = "Bubble"; 57 58 private final String mKey; 59 60 private long mLastUpdated; 61 private long mLastAccessed; 62 63 @Nullable 64 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; 65 66 /** Whether the bubble should show a dot for the notification indicating updated content. */ 67 private boolean mShowBubbleUpdateDot = true; 68 69 /** Whether flyout text should be suppressed, regardless of any other flags or state. */ 70 private boolean mSuppressFlyout; 71 72 // Items that are typically loaded later 73 private String mAppName; 74 private ShortcutInfo mShortcutInfo; 75 private String mMetadataShortcutId; 76 private BadgedImageView mIconView; 77 private BubbleExpandedView mExpandedView; 78 79 private BubbleViewInfoTask mInflationTask; 80 private boolean mInflateSynchronously; 81 private boolean mPendingIntentCanceled; 82 private boolean mIsImportantConversation; 83 84 /** 85 * Presentational info about the flyout. 86 */ 87 public static class FlyoutMessage { 88 @Nullable public Icon senderIcon; 89 @Nullable public Drawable senderAvatar; 90 @Nullable public CharSequence senderName; 91 @Nullable public CharSequence message; 92 @Nullable public boolean isGroupChat; 93 } 94 95 private FlyoutMessage mFlyoutMessage; 96 private Drawable mBadgedAppIcon; 97 private Bitmap mBadgedImage; 98 private int mDotColor; 99 private Path mDotPath; 100 private int mFlags; 101 102 @NonNull 103 private UserHandle mUser; 104 @NonNull 105 private String mPackageName; 106 @Nullable 107 private String mTitle; 108 @Nullable 109 private Icon mIcon; 110 private boolean mIsBubble; 111 private boolean mIsVisuallyInterruptive; 112 private boolean mIsClearable; 113 private boolean mShouldSuppressNotificationDot; 114 private boolean mShouldSuppressNotificationList; 115 private boolean mShouldSuppressPeek; 116 private int mDesiredHeight; 117 @DimenRes 118 private int mDesiredHeightResId; 119 120 /** for logging **/ 121 @Nullable 122 private InstanceId mInstanceId; 123 @Nullable 124 private String mChannelId; 125 private int mNotificationId; 126 private int mAppUid = -1; 127 128 /** 129 * A bubble is created and can be updated. This intent is updated until the user first 130 * expands the bubble. Once the user has expanded the contents, we ignore the intent updates 131 * to prevent restarting the intent & possibly altering UI state in the activity in front of 132 * the user. 133 * 134 * Once the bubble is overflowed, the activity is finished and updates to the 135 * notification are respected. Typically an update to an overflowed bubble would result in 136 * that bubble being added back to the stack anyways. 137 */ 138 @Nullable 139 private PendingIntent mIntent; 140 private boolean mIntentActive; 141 @Nullable 142 private PendingIntent.CancelListener mIntentCancelListener; 143 144 /** 145 * Sent when the bubble & notification are no longer visible to the user (i.e. no 146 * notification in the shade, no bubble in the stack or overflow). 147 */ 148 @Nullable 149 private PendingIntent mDeleteIntent; 150 151 /** 152 * Create a bubble with limited information based on given {@link ShortcutInfo}. 153 * Note: Currently this is only being used when the bubble is persisted to disk. 154 */ Bubble(@onNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title)155 Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, 156 final int desiredHeight, final int desiredHeightResId, @Nullable final String title) { 157 Objects.requireNonNull(key); 158 Objects.requireNonNull(shortcutInfo); 159 mMetadataShortcutId = shortcutInfo.getId(); 160 mShortcutInfo = shortcutInfo; 161 mKey = key; 162 mFlags = 0; 163 mUser = shortcutInfo.getUserHandle(); 164 mPackageName = shortcutInfo.getPackage(); 165 mIcon = shortcutInfo.getIcon(); 166 mDesiredHeight = desiredHeight; 167 mDesiredHeightResId = desiredHeightResId; 168 mTitle = title; 169 mShowBubbleUpdateDot = false; 170 } 171 172 @VisibleForTesting(visibility = PRIVATE) Bubble(@onNull final NotificationEntry e, @Nullable final BubbleController.NotificationSuppressionChangedListener listener, final BubbleController.PendingIntentCanceledListener intentCancelListener)173 Bubble(@NonNull final NotificationEntry e, 174 @Nullable final BubbleController.NotificationSuppressionChangedListener listener, 175 final BubbleController.PendingIntentCanceledListener intentCancelListener) { 176 Objects.requireNonNull(e); 177 mKey = e.getKey(); 178 mSuppressionListener = listener; 179 mIntentCancelListener = intent -> { 180 if (mIntent != null) { 181 mIntent.unregisterCancelListener(mIntentCancelListener); 182 } 183 intentCancelListener.onPendingIntentCanceled(this); 184 }; 185 setEntry(e); 186 } 187 188 @Override getKey()189 public String getKey() { 190 return mKey; 191 } 192 getUser()193 public UserHandle getUser() { 194 return mUser; 195 } 196 197 @NonNull getPackageName()198 public String getPackageName() { 199 return mPackageName; 200 } 201 202 @Override getBadgedImage()203 public Bitmap getBadgedImage() { 204 return mBadgedImage; 205 } 206 getBadgedAppIcon()207 public Drawable getBadgedAppIcon() { 208 return mBadgedAppIcon; 209 } 210 211 @Override getDotColor()212 public int getDotColor() { 213 return mDotColor; 214 } 215 216 @Override getDotPath()217 public Path getDotPath() { 218 return mDotPath; 219 } 220 221 @Nullable getAppName()222 public String getAppName() { 223 return mAppName; 224 } 225 226 @Nullable getShortcutInfo()227 public ShortcutInfo getShortcutInfo() { 228 return mShortcutInfo; 229 } 230 231 @Nullable 232 @Override getIconView()233 public BadgedImageView getIconView() { 234 return mIconView; 235 } 236 237 @Override 238 @Nullable getExpandedView()239 public BubbleExpandedView getExpandedView() { 240 return mExpandedView; 241 } 242 243 @Nullable getTitle()244 public String getTitle() { 245 return mTitle; 246 } 247 getMetadataShortcutId()248 String getMetadataShortcutId() { 249 return mMetadataShortcutId; 250 } 251 hasMetadataShortcutId()252 boolean hasMetadataShortcutId() { 253 return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); 254 } 255 256 /** 257 * Call when the views should be removed, ensure this is called to clean up ActivityView 258 * content. 259 */ cleanupViews()260 void cleanupViews() { 261 if (mExpandedView != null) { 262 mExpandedView.cleanUpExpandedState(); 263 mExpandedView = null; 264 } 265 mIconView = null; 266 if (mIntent != null) { 267 mIntent.unregisterCancelListener(mIntentCancelListener); 268 } 269 mIntentActive = false; 270 } 271 setPendingIntentCanceled()272 void setPendingIntentCanceled() { 273 mPendingIntentCanceled = true; 274 } 275 getPendingIntentCanceled()276 boolean getPendingIntentCanceled() { 277 return mPendingIntentCanceled; 278 } 279 280 /** 281 * Sets whether to perform inflation on the same thread as the caller. This method should only 282 * be used in tests, not in production. 283 */ 284 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)285 void setInflateSynchronously(boolean inflateSynchronously) { 286 mInflateSynchronously = inflateSynchronously; 287 } 288 289 /** 290 * Sets whether this bubble is considered visually interruptive. Normally pulled from the 291 * {@link NotificationEntry}, this method is purely for testing. 292 */ 293 @VisibleForTesting setVisuallyInterruptiveForTest(boolean visuallyInterruptive)294 void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { 295 mIsVisuallyInterruptive = visuallyInterruptive; 296 } 297 298 /** 299 * Starts a task to inflate & load any necessary information to display a bubble. 300 * 301 * @param callback the callback to notify one the bubble is ready to be displayed. 302 * @param context the context for the bubble. 303 * @param stackView the stackView the bubble is eventually added to. 304 * @param iconFactory the iconfactory use to create badged images for the bubble. 305 */ inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleStackView stackView, BubbleIconFactory iconFactory, boolean skipInflation)306 void inflate(BubbleViewInfoTask.Callback callback, 307 Context context, 308 BubbleStackView stackView, 309 BubbleIconFactory iconFactory, 310 boolean skipInflation) { 311 if (isBubbleLoading()) { 312 mInflationTask.cancel(true /* mayInterruptIfRunning */); 313 } 314 mInflationTask = new BubbleViewInfoTask(this, 315 context, 316 stackView, 317 iconFactory, 318 skipInflation, 319 callback); 320 if (mInflateSynchronously) { 321 mInflationTask.onPostExecute(mInflationTask.doInBackground()); 322 } else { 323 mInflationTask.execute(); 324 } 325 } 326 isBubbleLoading()327 private boolean isBubbleLoading() { 328 return mInflationTask != null && mInflationTask.getStatus() != FINISHED; 329 } 330 isInflated()331 boolean isInflated() { 332 return mIconView != null && mExpandedView != null; 333 } 334 stopInflation()335 void stopInflation() { 336 if (mInflationTask == null) { 337 return; 338 } 339 mInflationTask.cancel(true /* mayInterruptIfRunning */); 340 cleanupViews(); 341 } 342 setViewInfo(BubbleViewInfoTask.BubbleViewInfo info)343 void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { 344 if (!isInflated()) { 345 mIconView = info.imageView; 346 mExpandedView = info.expandedView; 347 } 348 349 mShortcutInfo = info.shortcutInfo; 350 mAppName = info.appName; 351 mFlyoutMessage = info.flyoutMessage; 352 353 mBadgedAppIcon = info.badgedAppIcon; 354 mBadgedImage = info.badgedBubbleImage; 355 mDotColor = info.dotColor; 356 mDotPath = info.dotPath; 357 358 if (mExpandedView != null) { 359 mExpandedView.update(this /* bubble */); 360 } 361 if (mIconView != null) { 362 mIconView.setRenderedBubble(this /* bubble */); 363 } 364 } 365 366 /** 367 * Set visibility of bubble in the expanded state. 368 * 369 * @param visibility {@code true} if the expanded bubble should be visible on the screen. 370 * 371 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 372 * and setting {@code false} actually means rendering the expanded view in transparent. 373 */ 374 @Override setContentVisibility(boolean visibility)375 public void setContentVisibility(boolean visibility) { 376 if (mExpandedView != null) { 377 mExpandedView.setContentVisibility(visibility); 378 } 379 } 380 381 /** 382 * Sets the entry associated with this bubble. 383 */ setEntry(@onNull final NotificationEntry entry)384 void setEntry(@NonNull final NotificationEntry entry) { 385 Objects.requireNonNull(entry); 386 Objects.requireNonNull(entry.getSbn()); 387 mLastUpdated = entry.getSbn().getPostTime(); 388 mIsBubble = entry.getSbn().getNotification().isBubbleNotification(); 389 mPackageName = entry.getSbn().getPackageName(); 390 mUser = entry.getSbn().getUser(); 391 mTitle = getTitle(entry); 392 mIsClearable = entry.isClearable(); 393 mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); 394 mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); 395 mShouldSuppressPeek = entry.shouldSuppressPeek(); 396 mChannelId = entry.getSbn().getNotification().getChannelId(); 397 mNotificationId = entry.getSbn().getId(); 398 mAppUid = entry.getSbn().getUid(); 399 mInstanceId = entry.getSbn().getInstanceId(); 400 mFlyoutMessage = BubbleViewInfoTask.extractFlyoutMessage(entry); 401 mShortcutInfo = (entry.getRanking() != null ? entry.getRanking().getShortcutInfo() : null); 402 mMetadataShortcutId = (entry.getBubbleMetadata() != null 403 ? entry.getBubbleMetadata().getShortcutId() : null); 404 if (entry.getRanking() != null) { 405 mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); 406 } 407 if (entry.getBubbleMetadata() != null) { 408 mFlags = entry.getBubbleMetadata().getFlags(); 409 mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); 410 mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); 411 mIcon = entry.getBubbleMetadata().getIcon(); 412 413 if (!mIntentActive || mIntent == null) { 414 if (mIntent != null) { 415 mIntent.unregisterCancelListener(mIntentCancelListener); 416 } 417 mIntent = entry.getBubbleMetadata().getIntent(); 418 if (mIntent != null) { 419 mIntent.registerCancelListener(mIntentCancelListener); 420 } 421 } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { 422 // Was an intent bubble now it's a shortcut bubble... still unregister the listener 423 mIntent.unregisterCancelListener(mIntentCancelListener); 424 mIntentActive = false; 425 mIntent = null; 426 } 427 mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); 428 } 429 mIsImportantConversation = 430 entry.getChannel() != null && entry.getChannel().isImportantConversation(); 431 } 432 433 @Nullable getIcon()434 Icon getIcon() { 435 return mIcon; 436 } 437 isVisuallyInterruptive()438 boolean isVisuallyInterruptive() { 439 return mIsVisuallyInterruptive; 440 } 441 442 /** 443 * @return the last time this bubble was updated or accessed, whichever is most recent. 444 */ getLastActivity()445 long getLastActivity() { 446 return Math.max(mLastUpdated, mLastAccessed); 447 } 448 449 /** 450 * Sets if the intent used for this bubble is currently active (i.e. populating an 451 * expanded view, expanded or not). 452 */ setIntentActive()453 void setIntentActive() { 454 mIntentActive = true; 455 } 456 isIntentActive()457 boolean isIntentActive() { 458 return mIntentActive; 459 } 460 461 /** 462 * @return the display id of the virtual display on which bubble contents is drawn. 463 */ 464 @Override getDisplayId()465 public int getDisplayId() { 466 return mExpandedView != null ? mExpandedView.getVirtualDisplayId() : INVALID_DISPLAY; 467 } 468 getInstanceId()469 public InstanceId getInstanceId() { 470 return mInstanceId; 471 } 472 473 @Nullable getChannelId()474 public String getChannelId() { 475 return mChannelId; 476 } 477 getNotificationId()478 public int getNotificationId() { 479 return mNotificationId; 480 } 481 482 /** 483 * Should be invoked whenever a Bubble is accessed (selected while expanded). 484 */ markAsAccessedAt(long lastAccessedMillis)485 void markAsAccessedAt(long lastAccessedMillis) { 486 mLastAccessed = lastAccessedMillis; 487 setSuppressNotification(true); 488 setShowDot(false /* show */); 489 } 490 491 /** 492 * Should be invoked whenever a Bubble is promoted from overflow. 493 */ markUpdatedAt(long lastAccessedMillis)494 void markUpdatedAt(long lastAccessedMillis) { 495 mLastUpdated = lastAccessedMillis; 496 } 497 498 /** 499 * Whether this notification should be shown in the shade. 500 */ showInShade()501 boolean showInShade() { 502 return !shouldSuppressNotification() || !mIsClearable; 503 } 504 505 /** 506 * Whether this notification conversation is important. 507 */ isImportantConversation()508 boolean isImportantConversation() { 509 return mIsImportantConversation; 510 } 511 512 /** 513 * Sets whether this notification should be suppressed in the shade. 514 */ setSuppressNotification(boolean suppressNotification)515 void setSuppressNotification(boolean suppressNotification) { 516 boolean prevShowInShade = showInShade(); 517 if (suppressNotification) { 518 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 519 } else { 520 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 521 } 522 523 if (showInShade() != prevShowInShade && mSuppressionListener != null) { 524 mSuppressionListener.onBubbleNotificationSuppressionChange(this); 525 } 526 } 527 528 /** 529 * Sets whether the bubble for this notification should show a dot indicating updated content. 530 */ setShowDot(boolean showDot)531 void setShowDot(boolean showDot) { 532 mShowBubbleUpdateDot = showDot; 533 534 if (mIconView != null) { 535 mIconView.updateDotVisibility(true /* animate */); 536 } 537 } 538 539 /** 540 * Whether the bubble for this notification should show a dot indicating updated content. 541 */ 542 @Override showDot()543 public boolean showDot() { 544 return mShowBubbleUpdateDot 545 && !mShouldSuppressNotificationDot 546 && !shouldSuppressNotification(); 547 } 548 549 /** 550 * Whether the flyout for the bubble should be shown. 551 */ showFlyout()552 boolean showFlyout() { 553 return !mSuppressFlyout && !mShouldSuppressPeek 554 && !shouldSuppressNotification() 555 && !mShouldSuppressNotificationList; 556 } 557 558 /** 559 * Set whether the flyout text for the bubble should be shown when an update is received. 560 * 561 * @param suppressFlyout whether the flyout text is shown 562 */ setSuppressFlyout(boolean suppressFlyout)563 void setSuppressFlyout(boolean suppressFlyout) { 564 mSuppressFlyout = suppressFlyout; 565 } 566 getFlyoutMessage()567 FlyoutMessage getFlyoutMessage() { 568 return mFlyoutMessage; 569 } 570 getRawDesiredHeight()571 int getRawDesiredHeight() { 572 return mDesiredHeight; 573 } 574 getRawDesiredHeightResId()575 int getRawDesiredHeightResId() { 576 return mDesiredHeightResId; 577 } 578 getDesiredHeight(Context context)579 float getDesiredHeight(Context context) { 580 boolean useRes = mDesiredHeightResId != 0; 581 if (useRes) { 582 return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName, 583 mUser.getIdentifier()); 584 } else { 585 return mDesiredHeight * context.getResources().getDisplayMetrics().density; 586 } 587 } 588 getDesiredHeightString()589 String getDesiredHeightString() { 590 boolean useRes = mDesiredHeightResId != 0; 591 if (useRes) { 592 return String.valueOf(mDesiredHeightResId); 593 } else { 594 return String.valueOf(mDesiredHeight); 595 } 596 } 597 598 @Nullable getBubbleIntent()599 PendingIntent getBubbleIntent() { 600 return mIntent; 601 } 602 603 @Nullable getDeleteIntent()604 PendingIntent getDeleteIntent() { 605 return mDeleteIntent; 606 } 607 getSettingsIntent(final Context context)608 Intent getSettingsIntent(final Context context) { 609 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); 610 intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); 611 final int uid = getUid(context); 612 if (uid != -1) { 613 intent.putExtra(Settings.EXTRA_APP_UID, uid); 614 } 615 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 616 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 617 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 618 return intent; 619 } 620 getAppUid()621 public int getAppUid() { 622 return mAppUid; 623 } 624 getUid(final Context context)625 private int getUid(final Context context) { 626 if (mAppUid != -1) return mAppUid; 627 final PackageManager pm = StatusBar.getPackageManagerForUser(context, 628 mUser.getIdentifier()); 629 if (pm == null) return -1; 630 try { 631 final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0); 632 return info.uid; 633 } catch (PackageManager.NameNotFoundException e) { 634 Log.e(TAG, "cannot find uid", e); 635 } 636 return -1; 637 } 638 getDimenForPackageUser(Context context, int resId, String pkg, int userId)639 private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { 640 PackageManager pm = context.getPackageManager(); 641 Resources r; 642 if (pkg != null) { 643 try { 644 if (userId == UserHandle.USER_ALL) { 645 userId = UserHandle.USER_SYSTEM; 646 } 647 r = pm.getResourcesForApplicationAsUser(pkg, userId); 648 return r.getDimensionPixelSize(resId); 649 } catch (PackageManager.NameNotFoundException ex) { 650 // Uninstalled, don't care 651 } catch (Resources.NotFoundException e) { 652 // Invalid res id, return 0 and user our default 653 Log.e(TAG, "Couldn't find desired height res id", e); 654 } 655 } 656 return 0; 657 } 658 shouldSuppressNotification()659 private boolean shouldSuppressNotification() { 660 return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); 661 } 662 shouldAutoExpand()663 public boolean shouldAutoExpand() { 664 return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 665 } 666 setShouldAutoExpand(boolean shouldAutoExpand)667 void setShouldAutoExpand(boolean shouldAutoExpand) { 668 if (shouldAutoExpand) { 669 enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 670 } else { 671 disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 672 } 673 } 674 setIsBubble(final boolean isBubble)675 public void setIsBubble(final boolean isBubble) { 676 mIsBubble = isBubble; 677 } 678 isBubble()679 public boolean isBubble() { 680 return mIsBubble; 681 } 682 enable(int option)683 public void enable(int option) { 684 mFlags |= option; 685 } 686 disable(int option)687 public void disable(int option) { 688 mFlags &= ~option; 689 } 690 isEnabled(int option)691 public boolean isEnabled(int option) { 692 return (mFlags & option) != 0; 693 } 694 695 @Override toString()696 public String toString() { 697 return "Bubble{" + mKey + '}'; 698 } 699 700 /** 701 * Description of current bubble state. 702 */ dump( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)703 public void dump( 704 @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 705 pw.print("key: "); pw.println(mKey); 706 pw.print(" showInShade: "); pw.println(showInShade()); 707 pw.print(" showDot: "); pw.println(showDot()); 708 pw.print(" showFlyout: "); pw.println(showFlyout()); 709 pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); 710 pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); 711 pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); 712 } 713 714 @Override equals(Object o)715 public boolean equals(Object o) { 716 if (this == o) return true; 717 if (!(o instanceof Bubble)) return false; 718 Bubble bubble = (Bubble) o; 719 return Objects.equals(mKey, bubble.mKey); 720 } 721 722 @Override hashCode()723 public int hashCode() { 724 return Objects.hash(mKey); 725 } 726 727 @Override logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index)728 public void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index) { 729 SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, 730 mPackageName, 731 mChannelId, 732 mNotificationId, 733 index, 734 bubbleCount, 735 action, 736 normalX, 737 normalY, 738 showInShade(), 739 false /* isOngoing (unused) */, 740 false /* isAppForeground (unused) */); 741 } 742 743 @Nullable getTitle(@onNull final NotificationEntry e)744 private static String getTitle(@NonNull final NotificationEntry e) { 745 final CharSequence titleCharSeq = e.getSbn().getNotification().extras.getCharSequence( 746 Notification.EXTRA_TITLE); 747 return titleCharSeq == null ? null : titleCharSeq.toString(); 748 } 749 } 750