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