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