• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
23 import android.annotation.DimenRes;
24 import android.annotation.Hide;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.Notification;
28 import android.app.PendingIntent;
29 import android.app.Person;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.LocusId;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ShortcutInfo;
36 import android.content.res.Resources;
37 import android.graphics.Bitmap;
38 import android.graphics.Path;
39 import android.graphics.drawable.Drawable;
40 import android.graphics.drawable.Icon;
41 import android.os.Parcelable;
42 import android.os.UserHandle;
43 import android.provider.Settings;
44 import android.service.notification.StatusBarNotification;
45 import android.text.TextUtils;
46 import android.util.Log;
47 
48 import com.android.internal.annotations.VisibleForTesting;
49 import com.android.internal.logging.InstanceId;
50 
51 import java.io.PrintWriter;
52 import java.util.List;
53 import java.util.Objects;
54 import java.util.concurrent.Executor;
55 
56 /**
57  * Encapsulates the data and UI elements of a bubble.
58  */
59 @VisibleForTesting
60 public class Bubble implements BubbleViewProvider {
61     private static final String TAG = "Bubble";
62 
63     public static final String KEY_APP_BUBBLE = "key_app_bubble";
64 
65     private final String mKey;
66     @Nullable
67     private final String mGroupKey;
68     @Nullable
69     private final LocusId mLocusId;
70 
71     private final Executor mMainExecutor;
72 
73     private long mLastUpdated;
74     private long mLastAccessed;
75 
76     @Nullable
77     private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
78 
79     /** Whether the bubble should show a dot for the notification indicating updated content. */
80     private boolean mShowBubbleUpdateDot = true;
81 
82     /** Whether flyout text should be suppressed, regardless of any other flags or state. */
83     private boolean mSuppressFlyout;
84 
85     // Items that are typically loaded later
86     private String mAppName;
87     private ShortcutInfo mShortcutInfo;
88     private String mMetadataShortcutId;
89     private BadgedImageView mIconView;
90     private BubbleExpandedView mExpandedView;
91 
92     private BubbleViewInfoTask mInflationTask;
93     private boolean mInflateSynchronously;
94     private boolean mPendingIntentCanceled;
95     private boolean mIsImportantConversation;
96 
97     /**
98      * Presentational info about the flyout.
99      */
100     public static class FlyoutMessage {
101         @Nullable public Icon senderIcon;
102         @Nullable public Drawable senderAvatar;
103         @Nullable public CharSequence senderName;
104         @Nullable public CharSequence message;
105         @Nullable public boolean isGroupChat;
106     }
107 
108     private FlyoutMessage mFlyoutMessage;
109     // The developer provided image for the bubble
110     private Bitmap mBubbleBitmap;
111     // The app badge for the bubble
112     private Bitmap mBadgeBitmap;
113     // App badge without any markings for important conversations
114     private Bitmap mRawBadgeBitmap;
115     private int mDotColor;
116     private Path mDotPath;
117     private int mFlags;
118 
119     @NonNull
120     private UserHandle mUser;
121     @NonNull
122     private String mPackageName;
123     @Nullable
124     private String mTitle;
125     @Nullable
126     private Icon mIcon;
127     private boolean mIsBubble;
128     private boolean mIsTextChanged;
129     private boolean mIsDismissable;
130     private boolean mShouldSuppressNotificationDot;
131     private boolean mShouldSuppressNotificationList;
132     private boolean mShouldSuppressPeek;
133     private int mDesiredHeight;
134     @DimenRes
135     private int mDesiredHeightResId;
136     private int mTaskId;
137 
138     /** for logging **/
139     @Nullable
140     private InstanceId mInstanceId;
141     @Nullable
142     private String mChannelId;
143     private int mNotificationId;
144     private int mAppUid = -1;
145 
146     /**
147      * A bubble is created and can be updated. This intent is updated until the user first
148      * expands the bubble. Once the user has expanded the contents, we ignore the intent updates
149      * to prevent restarting the intent & possibly altering UI state in the activity in front of
150      * the user.
151      *
152      * Once the bubble is overflowed, the activity is finished and updates to the
153      * notification are respected. Typically an update to an overflowed bubble would result in
154      * that bubble being added back to the stack anyways.
155      */
156     @Nullable
157     private PendingIntent mIntent;
158     private boolean mIntentActive;
159     @Nullable
160     private PendingIntent.CancelListener mIntentCancelListener;
161 
162     /**
163      * Sent when the bubble & notification are no longer visible to the user (i.e. no
164      * notification in the shade, no bubble in the stack or overflow).
165      */
166     @Nullable
167     private PendingIntent mDeleteIntent;
168 
169     /**
170      * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}.
171      * There can only be one of these bubbles in the stack and this intent will be populated for
172      * that bubble.
173      */
174     @Nullable
175     private Intent mAppIntent;
176 
177     /**
178      * Create a bubble with limited information based on given {@link ShortcutInfo}.
179      * Note: Currently this is only being used when the bubble is persisted to disk.
180      */
181     @VisibleForTesting(visibility = PRIVATE)
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, Executor mainExecutor, final Bubbles.BubbleMetadataFlagListener listener)182     public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
183             final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
184             int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor,
185             final Bubbles.BubbleMetadataFlagListener listener) {
186         Objects.requireNonNull(key);
187         Objects.requireNonNull(shortcutInfo);
188         mMetadataShortcutId = shortcutInfo.getId();
189         mShortcutInfo = shortcutInfo;
190         mKey = key;
191         mGroupKey = null;
192         mLocusId = locus != null ? new LocusId(locus) : null;
193         mIsDismissable = isDismissable;
194         mFlags = 0;
195         mUser = shortcutInfo.getUserHandle();
196         mPackageName = shortcutInfo.getPackage();
197         mIcon = shortcutInfo.getIcon();
198         mDesiredHeight = desiredHeight;
199         mDesiredHeightResId = desiredHeightResId;
200         mTitle = title;
201         mShowBubbleUpdateDot = false;
202         mMainExecutor = mainExecutor;
203         mTaskId = taskId;
204         mBubbleMetadataFlagListener = listener;
205     }
206 
Bubble(Intent intent, UserHandle user, Executor mainExecutor)207     public Bubble(Intent intent,
208             UserHandle user,
209             Executor mainExecutor) {
210         mKey = KEY_APP_BUBBLE;
211         mGroupKey = null;
212         mLocusId = null;
213         mFlags = 0;
214         mUser = user;
215         mShowBubbleUpdateDot = false;
216         mMainExecutor = mainExecutor;
217         mTaskId = INVALID_TASK_ID;
218         mAppIntent = intent;
219         mDesiredHeight = Integer.MAX_VALUE;
220         mPackageName = intent.getPackage();
221     }
222 
223     @VisibleForTesting(visibility = PRIVATE)
Bubble(@onNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor)224     public Bubble(@NonNull final BubbleEntry entry,
225             final Bubbles.BubbleMetadataFlagListener listener,
226             final Bubbles.PendingIntentCanceledListener intentCancelListener,
227             Executor mainExecutor) {
228         mKey = entry.getKey();
229         mGroupKey = entry.getGroupKey();
230         mLocusId = entry.getLocusId();
231         mBubbleMetadataFlagListener = listener;
232         mIntentCancelListener = intent -> {
233             if (mIntent != null) {
234                 mIntent.unregisterCancelListener(mIntentCancelListener);
235             }
236             mainExecutor.execute(() -> {
237                 intentCancelListener.onPendingIntentCanceled(this);
238             });
239         };
240         mMainExecutor = mainExecutor;
241         mTaskId = INVALID_TASK_ID;
242         setEntry(entry);
243     }
244 
245     @Override
getKey()246     public String getKey() {
247         return mKey;
248     }
249 
250     @Hide
isDismissable()251     public boolean isDismissable() {
252         return mIsDismissable;
253     }
254 
255     /**
256      * @see StatusBarNotification#getGroupKey()
257      * @return the group key for this bubble, if one exists.
258      */
getGroupKey()259     public String getGroupKey() {
260         return mGroupKey;
261     }
262 
getLocusId()263     public LocusId getLocusId() {
264         return mLocusId;
265     }
266 
getUser()267     public UserHandle getUser() {
268         return mUser;
269     }
270 
271     @NonNull
getPackageName()272     public String getPackageName() {
273         return mPackageName;
274     }
275 
276     @Override
getBubbleIcon()277     public Bitmap getBubbleIcon() {
278         return mBubbleBitmap;
279     }
280 
281     @Override
getAppBadge()282     public Bitmap getAppBadge() {
283         return mBadgeBitmap;
284     }
285 
286     @Override
getRawAppBadge()287     public Bitmap getRawAppBadge() {
288         return mRawBadgeBitmap;
289     }
290 
291     @Override
getDotColor()292     public int getDotColor() {
293         return mDotColor;
294     }
295 
296     @Override
getDotPath()297     public Path getDotPath() {
298         return mDotPath;
299     }
300 
301     @Nullable
getAppName()302     public String getAppName() {
303         return mAppName;
304     }
305 
306     @Nullable
getShortcutInfo()307     public ShortcutInfo getShortcutInfo() {
308         return mShortcutInfo;
309     }
310 
311     @Nullable
312     @Override
getIconView()313     public BadgedImageView getIconView() {
314         return mIconView;
315     }
316 
317     @Override
318     @Nullable
getExpandedView()319     public BubbleExpandedView getExpandedView() {
320         return mExpandedView;
321     }
322 
323     @Nullable
getTitle()324     public String getTitle() {
325         return mTitle;
326     }
327 
328     /**
329      * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
330      */
getShortcutId()331     String getShortcutId() {
332         return getShortcutInfo() != null
333                 ? getShortcutInfo().getId()
334                 : getMetadataShortcutId();
335     }
336 
getMetadataShortcutId()337     String getMetadataShortcutId() {
338         return mMetadataShortcutId;
339     }
340 
hasMetadataShortcutId()341     boolean hasMetadataShortcutId() {
342         return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty());
343     }
344 
345     /**
346      * Call this to clean up the task for the bubble. Ensure this is always called when done with
347      * the bubble.
348      */
cleanupExpandedView()349     void cleanupExpandedView() {
350         if (mExpandedView != null) {
351             mExpandedView.cleanUpExpandedState();
352             mExpandedView = null;
353         }
354         if (mIntent != null) {
355             mIntent.unregisterCancelListener(mIntentCancelListener);
356         }
357         mIntentActive = false;
358     }
359 
360     /**
361      * Call when all the views should be removed/cleaned up.
362      */
cleanupViews()363     void cleanupViews() {
364         cleanupExpandedView();
365         mIconView = null;
366     }
367 
setPendingIntentCanceled()368     void setPendingIntentCanceled() {
369         mPendingIntentCanceled = true;
370     }
371 
getPendingIntentCanceled()372     boolean getPendingIntentCanceled() {
373         return mPendingIntentCanceled;
374     }
375 
376     /**
377      * Sets whether to perform inflation on the same thread as the caller. This method should only
378      * be used in tests, not in production.
379      */
380     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)381     void setInflateSynchronously(boolean inflateSynchronously) {
382         mInflateSynchronously = inflateSynchronously;
383     }
384 
385     /**
386      * Sets whether this bubble is considered text changed. This method is purely for
387      * testing.
388      */
389     @VisibleForTesting
setTextChangedForTest(boolean textChanged)390     void setTextChangedForTest(boolean textChanged) {
391         mIsTextChanged = textChanged;
392     }
393 
394     /**
395      * Starts a task to inflate & load any necessary information to display a bubble.
396      *
397      * @param callback the callback to notify one the bubble is ready to be displayed.
398      * @param context the context for the bubble.
399      * @param controller the bubble controller.
400      * @param stackView the stackView the bubble is eventually added to.
401      * @param iconFactory the icon factory use to create images for the bubble.
402      * @param badgeIconFactory the icon factory to create app badges for the bubble.
403      */
inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, BubbleBadgeIconFactory badgeIconFactory, boolean skipInflation)404     void inflate(BubbleViewInfoTask.Callback callback,
405             Context context,
406             BubbleController controller,
407             BubbleStackView stackView,
408             BubbleIconFactory iconFactory,
409             BubbleBadgeIconFactory badgeIconFactory,
410             boolean skipInflation) {
411         if (isBubbleLoading()) {
412             mInflationTask.cancel(true /* mayInterruptIfRunning */);
413         }
414         mInflationTask = new BubbleViewInfoTask(this,
415                 context,
416                 controller,
417                 stackView,
418                 iconFactory,
419                 badgeIconFactory,
420                 skipInflation,
421                 callback,
422                 mMainExecutor);
423         if (mInflateSynchronously) {
424             mInflationTask.onPostExecute(mInflationTask.doInBackground());
425         } else {
426             mInflationTask.execute();
427         }
428     }
429 
isBubbleLoading()430     private boolean isBubbleLoading() {
431         return mInflationTask != null && mInflationTask.getStatus() != FINISHED;
432     }
433 
isInflated()434     boolean isInflated() {
435         return mIconView != null && mExpandedView != null;
436     }
437 
stopInflation()438     void stopInflation() {
439         if (mInflationTask == null) {
440             return;
441         }
442         mInflationTask.cancel(true /* mayInterruptIfRunning */);
443     }
444 
setViewInfo(BubbleViewInfoTask.BubbleViewInfo info)445     void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) {
446         if (!isInflated()) {
447             mIconView = info.imageView;
448             mExpandedView = info.expandedView;
449         }
450 
451         mShortcutInfo = info.shortcutInfo;
452         mAppName = info.appName;
453         if (mTitle == null) {
454             mTitle = mAppName;
455         }
456         mFlyoutMessage = info.flyoutMessage;
457 
458         mBadgeBitmap = info.badgeBitmap;
459         mRawBadgeBitmap = info.mRawBadgeBitmap;
460         mBubbleBitmap = info.bubbleBitmap;
461 
462         mDotColor = info.dotColor;
463         mDotPath = info.dotPath;
464 
465         if (mExpandedView != null) {
466             mExpandedView.update(this /* bubble */);
467         }
468         if (mIconView != null) {
469             mIconView.setRenderedBubble(this /* bubble */);
470         }
471     }
472 
473     /**
474      * Set visibility of bubble in the expanded state.
475      *
476      * @param visibility {@code true} if the expanded bubble should be visible on the screen.
477      *
478      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
479      * and setting {@code false} actually means rendering the expanded view in transparent.
480      */
481     @Override
setTaskViewVisibility(boolean visibility)482     public void setTaskViewVisibility(boolean visibility) {
483         if (mExpandedView != null) {
484             mExpandedView.setContentVisibility(visibility);
485         }
486     }
487 
488     /**
489      * Sets the entry associated with this bubble.
490      */
setEntry(@onNull final BubbleEntry entry)491     void setEntry(@NonNull final BubbleEntry entry) {
492         Objects.requireNonNull(entry);
493         boolean showingDotPreviously = showDot();
494         mLastUpdated = entry.getStatusBarNotification().getPostTime();
495         mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification();
496         mPackageName = entry.getStatusBarNotification().getPackageName();
497         mUser = entry.getStatusBarNotification().getUser();
498         mTitle = getTitle(entry);
499         mChannelId = entry.getStatusBarNotification().getNotification().getChannelId();
500         mNotificationId = entry.getStatusBarNotification().getId();
501         mAppUid = entry.getStatusBarNotification().getUid();
502         mInstanceId = entry.getStatusBarNotification().getInstanceId();
503         mFlyoutMessage = extractFlyoutMessage(entry);
504         if (entry.getRanking() != null) {
505             mShortcutInfo = entry.getRanking().getConversationShortcutInfo();
506             mIsTextChanged = entry.getRanking().isTextChanged();
507             if (entry.getRanking().getChannel() != null) {
508                 mIsImportantConversation =
509                         entry.getRanking().getChannel().isImportantConversation();
510             }
511         }
512         if (entry.getBubbleMetadata() != null) {
513             mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId();
514             mFlags = entry.getBubbleMetadata().getFlags();
515             mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight();
516             mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId();
517             mIcon = entry.getBubbleMetadata().getIcon();
518 
519             if (!mIntentActive || mIntent == null) {
520                 if (mIntent != null) {
521                     mIntent.unregisterCancelListener(mIntentCancelListener);
522                 }
523                 mIntent = entry.getBubbleMetadata().getIntent();
524                 if (mIntent != null) {
525                     mIntent.registerCancelListener(mIntentCancelListener);
526                 }
527             } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) {
528                 // Was an intent bubble now it's a shortcut bubble... still unregister the listener
529                 mIntent.unregisterCancelListener(mIntentCancelListener);
530                 mIntentActive = false;
531                 mIntent = null;
532             }
533             mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
534         }
535 
536         mIsDismissable = entry.isDismissable();
537         mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
538         mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
539         mShouldSuppressPeek = entry.shouldSuppressPeek();
540         if (showingDotPreviously != showDot()) {
541             // This will update the UI if needed
542             setShowDot(showDot());
543         }
544     }
545 
546     @Nullable
getIcon()547     Icon getIcon() {
548         return mIcon;
549     }
550 
isTextChanged()551     boolean isTextChanged() {
552         return mIsTextChanged;
553     }
554 
555     /**
556      * @return the last time this bubble was updated or accessed, whichever is most recent.
557      */
getLastActivity()558     long getLastActivity() {
559         return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed);
560     }
561 
562     /**
563      * Sets if the intent used for this bubble is currently active (i.e. populating an
564      * expanded view, expanded or not).
565      */
setIntentActive()566     void setIntentActive() {
567         mIntentActive = true;
568     }
569 
isIntentActive()570     boolean isIntentActive() {
571         return mIntentActive;
572     }
573 
getInstanceId()574     public InstanceId getInstanceId() {
575         return mInstanceId;
576     }
577 
578     @Nullable
getChannelId()579     public String getChannelId() {
580         return mChannelId;
581     }
582 
getNotificationId()583     public int getNotificationId() {
584         return mNotificationId;
585     }
586 
587     /**
588      * @return the task id of the task in which bubble contents is drawn.
589      */
590     @Override
getTaskId()591     public int getTaskId() {
592         return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId;
593     }
594 
595     /**
596      * Should be invoked whenever a Bubble is accessed (selected while expanded).
597      */
markAsAccessedAt(long lastAccessedMillis)598     void markAsAccessedAt(long lastAccessedMillis) {
599         mLastAccessed = lastAccessedMillis;
600         setSuppressNotification(true);
601         setShowDot(false /* show */);
602     }
603 
604     /**
605      * Should be invoked whenever a Bubble is promoted from overflow.
606      */
markUpdatedAt(long lastAccessedMillis)607     void markUpdatedAt(long lastAccessedMillis) {
608         mLastUpdated = lastAccessedMillis;
609     }
610 
611     /**
612      * Whether this notification should be shown in the shade.
613      */
showInShade()614     boolean showInShade() {
615         return !shouldSuppressNotification() || !mIsDismissable;
616     }
617 
618     /**
619      * Whether this bubble is currently being hidden from the stack.
620      */
isSuppressed()621     boolean isSuppressed() {
622         return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0;
623     }
624 
625     /**
626      * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API to
627      * hide the bubble when in the same content).
628      */
isSuppressable()629     boolean isSuppressable() {
630         return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0;
631     }
632 
633     /**
634      * Whether this notification conversation is important.
635      */
isImportantConversation()636     boolean isImportantConversation() {
637         return mIsImportantConversation;
638     }
639 
640     /**
641      * Sets whether this notification should be suppressed in the shade.
642      */
643     @VisibleForTesting
setSuppressNotification(boolean suppressNotification)644     public void setSuppressNotification(boolean suppressNotification) {
645         boolean prevShowInShade = showInShade();
646         if (suppressNotification) {
647             mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
648         } else {
649             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
650         }
651 
652         if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) {
653             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
654         }
655     }
656 
657     /**
658      * Sets whether this bubble should be suppressed from the stack.
659      */
setSuppressBubble(boolean suppressBubble)660     public void setSuppressBubble(boolean suppressBubble) {
661         if (!isSuppressable()) {
662             Log.e(TAG, "calling setSuppressBubble on "
663                     + getKey() + " when bubble not suppressable");
664             return;
665         }
666         boolean prevSuppressed = isSuppressed();
667         if (suppressBubble) {
668             mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
669         } else {
670             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
671         }
672         if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) {
673             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
674         }
675     }
676 
677     /**
678      * Sets whether the bubble for this notification should show a dot indicating updated content.
679      */
setShowDot(boolean showDot)680     void setShowDot(boolean showDot) {
681         mShowBubbleUpdateDot = showDot;
682 
683         if (mIconView != null) {
684             mIconView.updateDotVisibility(true /* animate */);
685         }
686     }
687 
688     /**
689      * Whether the bubble for this notification should show a dot indicating updated content.
690      */
691     @Override
showDot()692     public boolean showDot() {
693         return mShowBubbleUpdateDot
694                 && !mShouldSuppressNotificationDot
695                 && !shouldSuppressNotification();
696     }
697 
698     /**
699      * Whether the flyout for the bubble should be shown.
700      */
701     @VisibleForTesting
showFlyout()702     public boolean showFlyout() {
703         return !mSuppressFlyout && !mShouldSuppressPeek
704                 && !shouldSuppressNotification()
705                 && !mShouldSuppressNotificationList;
706     }
707 
708     /**
709      * Set whether the flyout text for the bubble should be shown when an update is received.
710      *
711      * @param suppressFlyout whether the flyout text is shown
712      */
setSuppressFlyout(boolean suppressFlyout)713     void setSuppressFlyout(boolean suppressFlyout) {
714         mSuppressFlyout = suppressFlyout;
715     }
716 
getFlyoutMessage()717     FlyoutMessage getFlyoutMessage() {
718         return mFlyoutMessage;
719     }
720 
getRawDesiredHeight()721     int getRawDesiredHeight() {
722         return mDesiredHeight;
723     }
724 
getRawDesiredHeightResId()725     int getRawDesiredHeightResId() {
726         return mDesiredHeightResId;
727     }
728 
getDesiredHeight(Context context)729     float getDesiredHeight(Context context) {
730         boolean useRes = mDesiredHeightResId != 0;
731         if (useRes) {
732             return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName,
733                     mUser.getIdentifier());
734         } else {
735             return mDesiredHeight * context.getResources().getDisplayMetrics().density;
736         }
737     }
738 
getDesiredHeightString()739     String getDesiredHeightString() {
740         boolean useRes = mDesiredHeightResId != 0;
741         if (useRes) {
742             return String.valueOf(mDesiredHeightResId);
743         } else {
744             return String.valueOf(mDesiredHeight);
745         }
746     }
747 
748     @Nullable
getBubbleIntent()749     PendingIntent getBubbleIntent() {
750         return mIntent;
751     }
752 
753     @Nullable
getDeleteIntent()754     PendingIntent getDeleteIntent() {
755         return mDeleteIntent;
756     }
757 
758     @Nullable
getAppBubbleIntent()759     Intent getAppBubbleIntent() {
760         return mAppIntent;
761     }
762 
isAppBubble()763     boolean isAppBubble() {
764         return KEY_APP_BUBBLE.equals(mKey);
765     }
766 
getSettingsIntent(final Context context)767     Intent getSettingsIntent(final Context context) {
768         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
769         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
770         final int uid = getUid(context);
771         if (uid != -1) {
772             intent.putExtra(Settings.EXTRA_APP_UID, uid);
773         }
774         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
775         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
776         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
777         return intent;
778     }
779 
getAppUid()780     public int getAppUid() {
781         return mAppUid;
782     }
783 
getUid(final Context context)784     private int getUid(final Context context) {
785         if (mAppUid != -1) return mAppUid;
786         final PackageManager pm = BubbleController.getPackageManagerForUser(context,
787                 mUser.getIdentifier());
788         if (pm == null) return -1;
789         try {
790             final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0);
791             return info.uid;
792         } catch (PackageManager.NameNotFoundException e) {
793             Log.e(TAG, "cannot find uid", e);
794         }
795         return -1;
796     }
797 
getDimenForPackageUser(Context context, int resId, String pkg, int userId)798     private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
799         Resources r;
800         if (pkg != null) {
801             try {
802                 if (userId == UserHandle.USER_ALL) {
803                     userId = UserHandle.USER_SYSTEM;
804                 }
805                 r = context.createContextAsUser(UserHandle.of(userId), /* flags */ 0)
806                         .getPackageManager().getResourcesForApplication(pkg);
807                 return r.getDimensionPixelSize(resId);
808             } catch (PackageManager.NameNotFoundException ex) {
809                 // Uninstalled, don't care
810             } catch (Resources.NotFoundException e) {
811                 // Invalid res id, return 0 and user our default
812                 Log.e(TAG, "Couldn't find desired height res id", e);
813             }
814         }
815         return 0;
816     }
817 
shouldSuppressNotification()818     private boolean shouldSuppressNotification() {
819         return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
820     }
821 
shouldAutoExpand()822     public boolean shouldAutoExpand() {
823         return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
824     }
825 
826     @VisibleForTesting
setShouldAutoExpand(boolean shouldAutoExpand)827     public void setShouldAutoExpand(boolean shouldAutoExpand) {
828         boolean prevAutoExpand = shouldAutoExpand();
829         if (shouldAutoExpand) {
830             enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
831         } else {
832             disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
833         }
834         if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) {
835             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
836         }
837     }
838 
setIsBubble(final boolean isBubble)839     public void setIsBubble(final boolean isBubble) {
840         mIsBubble = isBubble;
841     }
842 
isBubble()843     public boolean isBubble() {
844         return mIsBubble;
845     }
846 
enable(int option)847     public void enable(int option) {
848         mFlags |= option;
849     }
850 
disable(int option)851     public void disable(int option) {
852         mFlags &= ~option;
853     }
854 
isEnabled(int option)855     public boolean isEnabled(int option) {
856         return (mFlags & option) != 0;
857     }
858 
getFlags()859     public int getFlags() {
860         return mFlags;
861     }
862 
863     @Override
toString()864     public String toString() {
865         return "Bubble{" + mKey + '}';
866     }
867 
868     /**
869      * Description of current bubble state.
870      */
dump(@onNull PrintWriter pw)871     public void dump(@NonNull PrintWriter pw) {
872         pw.print("key: "); pw.println(mKey);
873         pw.print("  showInShade:   "); pw.println(showInShade());
874         pw.print("  showDot:       "); pw.println(showDot());
875         pw.print("  showFlyout:    "); pw.println(showFlyout());
876         pw.print("  lastActivity:  "); pw.println(getLastActivity());
877         pw.print("  desiredHeight: "); pw.println(getDesiredHeightString());
878         pw.print("  suppressNotif: "); pw.println(shouldSuppressNotification());
879         pw.print("  autoExpand:    "); pw.println(shouldAutoExpand());
880         pw.print("  isDismissable: "); pw.println(mIsDismissable);
881         pw.println("  bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null));
882         if (mExpandedView != null) {
883             mExpandedView.dump(pw);
884         }
885     }
886 
887     @Override
equals(Object o)888     public boolean equals(Object o) {
889         if (this == o) return true;
890         if (!(o instanceof Bubble)) return false;
891         Bubble bubble = (Bubble) o;
892         return Objects.equals(mKey, bubble.mKey);
893     }
894 
895     @Override
hashCode()896     public int hashCode() {
897         return Objects.hash(mKey);
898     }
899 
900     @Nullable
getTitle(@onNull final BubbleEntry e)901     private static String getTitle(@NonNull final BubbleEntry e) {
902         final CharSequence titleCharSeq = e.getStatusBarNotification()
903                 .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
904         return titleCharSeq == null ? null : titleCharSeq.toString();
905     }
906 
907     /**
908      * Returns our best guess for the most relevant text summary of the latest update to this
909      * notification, based on its type. Returns null if there should not be an update message.
910      */
911     @NonNull
extractFlyoutMessage(BubbleEntry entry)912     static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) {
913         Objects.requireNonNull(entry);
914         final Notification underlyingNotif = entry.getStatusBarNotification().getNotification();
915         final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
916 
917         Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
918         bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
919                 Notification.EXTRA_IS_GROUP_CONVERSATION);
920         try {
921             if (Notification.BigTextStyle.class.equals(style)) {
922                 // Return the big text, it is big so probably important. If it's not there use the
923                 // normal text.
924                 CharSequence bigText =
925                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
926                 bubbleMessage.message = !TextUtils.isEmpty(bigText)
927                         ? bigText
928                         : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
929                 return bubbleMessage;
930             } else if (Notification.MessagingStyle.class.equals(style)) {
931                 final List<Notification.MessagingStyle.Message> messages =
932                         Notification.MessagingStyle.Message.getMessagesFromBundleArray(
933                                 (Parcelable[]) underlyingNotif.extras.get(
934                                         Notification.EXTRA_MESSAGES));
935 
936                 final Notification.MessagingStyle.Message latestMessage =
937                         Notification.MessagingStyle.findLatestIncomingMessage(messages);
938                 if (latestMessage != null) {
939                     bubbleMessage.message = latestMessage.getText();
940                     Person sender = latestMessage.getSenderPerson();
941                     bubbleMessage.senderName = sender != null ? sender.getName() : null;
942                     bubbleMessage.senderAvatar = null;
943                     bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null;
944                     return bubbleMessage;
945                 }
946             } else if (Notification.InboxStyle.class.equals(style)) {
947                 CharSequence[] lines =
948                         underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
949 
950                 // Return the last line since it should be the most recent.
951                 if (lines != null && lines.length > 0) {
952                     bubbleMessage.message = lines[lines.length - 1];
953                     return bubbleMessage;
954                 }
955             } else if (Notification.MediaStyle.class.equals(style)) {
956                 // Return nothing, media updates aren't typically useful as a text update.
957                 return bubbleMessage;
958             } else {
959                 // Default to text extra.
960                 bubbleMessage.message =
961                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
962                 return bubbleMessage;
963             }
964         } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
965             // No use crashing, we'll just return null and the caller will assume there's no update
966             // message.
967             e.printStackTrace();
968         }
969 
970         return bubbleMessage;
971     }
972 }
973