• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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