• 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 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.app.TaskInfo;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.LocusId;
35 import android.content.pm.ApplicationInfo;
36 import android.content.pm.PackageManager;
37 import android.content.pm.ShortcutInfo;
38 import android.content.res.Resources;
39 import android.graphics.Bitmap;
40 import android.graphics.Path;
41 import android.graphics.drawable.Drawable;
42 import android.graphics.drawable.Icon;
43 import android.os.Parcelable;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.service.notification.StatusBarNotification;
47 import android.text.TextUtils;
48 import android.util.Log;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.logging.InstanceId;
52 import com.android.internal.protolog.ProtoLog;
53 import com.android.launcher3.icons.BubbleIconFactory;
54 import com.android.wm.shell.Flags;
55 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
56 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
57 import com.android.wm.shell.common.ComponentUtils;
58 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
59 import com.android.wm.shell.shared.annotations.ShellMainThread;
60 import com.android.wm.shell.shared.bubbles.BubbleInfo;
61 import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage;
62 import com.android.wm.shell.taskview.TaskView;
63 
64 import java.io.PrintWriter;
65 import java.util.List;
66 import java.util.Objects;
67 import java.util.concurrent.Executor;
68 
69 /**
70  * Encapsulates the data and UI elements of a bubble.
71  */
72 public class Bubble implements BubbleViewProvider {
73     private static final String TAG = "Bubble";
74 
75     /** A string prefix used in app bubbles' {@link #mKey}. */
76     public static final String KEY_APP_BUBBLE = "key_app_bubble";
77 
78     /** A string prefix used in note bubbles' {@link #mKey}. */
79     public static final String KEY_NOTE_BUBBLE = "key_note_bubble";
80 
81     /** The possible types a bubble may be. */
82     public enum BubbleType {
83         /** Chat is from a notification. */
84         TYPE_CHAT,
85         /** Notes are from the note taking API. */
86         TYPE_NOTE,
87         /** Shortcuts from bubble anything, based on {@link ShortcutInfo}. */
88         TYPE_SHORTCUT,
89         /** Apps are from bubble anything. */
90         TYPE_APP,
91     }
92 
93     private final BubbleType mType;
94 
95     private final String mKey;
96     @Nullable
97     private final String mGroupKey;
98     @Nullable
99     private final LocusId mLocusId;
100 
101     private final Executor mMainExecutor;
102     private final Executor mBgExecutor;
103 
104     private long mLastUpdated;
105     private long mLastAccessed;
106 
107     @Nullable
108     private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
109 
110     /** Whether the bubble should show a dot for the notification indicating updated content. */
111     private boolean mShowBubbleUpdateDot = true;
112 
113     /** Whether flyout text should be suppressed, regardless of any other flags or state. */
114     private boolean mSuppressFlyout;
115 
116     // Items that are typically loaded later
117     private String mAppName;
118     private ShortcutInfo mShortcutInfo;
119     private String mMetadataShortcutId;
120 
121     /**
122      * If {@link BubbleController#isShowingAsBubbleBar()} is true, the only view that will be
123      * populated will be {@link #mBubbleBarExpandedView}. If it is false, {@link #mIconView}
124      * and {@link #mExpandedView} will be populated.
125      */
126     @Nullable
127     private BadgedImageView mIconView;
128     @Nullable
129     private BubbleExpandedView mExpandedView;
130     @Nullable
131     private BubbleBarExpandedView mBubbleBarExpandedView;
132     @Nullable
133     private BubbleTaskView mBubbleTaskView;
134 
135     @Nullable
136     private BubbleViewInfoTask mInflationTask;
137     @Nullable
138     private BubbleViewInfoTaskLegacy mInflationTaskLegacy;
139     private boolean mInflateSynchronously;
140     private boolean mPendingIntentCanceled;
141     private boolean mIsImportantConversation;
142 
143     /**
144      * Presentational info about the flyout.
145      */
146     public static class FlyoutMessage {
147         @Nullable public Icon senderIcon;
148         @Nullable public Drawable senderAvatar;
149         @Nullable public CharSequence senderName;
150         @Nullable public CharSequence message;
151         @Nullable public boolean isGroupChat;
152     }
153 
154     private FlyoutMessage mFlyoutMessage;
155     // The developer provided image for the bubble
156     private Bitmap mBubbleBitmap;
157     // The app badge for the bubble
158     private Bitmap mBadgeBitmap;
159     // App badge without any markings for important conversations
160     private Bitmap mRawBadgeBitmap;
161     private int mDotColor;
162     private Path mDotPath;
163     private int mFlags;
164 
165     @NonNull
166     private UserHandle mUser;
167     @NonNull
168     private String mPackageName;
169     @Nullable
170     private String mTitle;
171     @Nullable
172     private Icon mIcon;
173     private boolean mIsBubble;
174     private boolean mIsTextChanged;
175     private boolean mIsDismissable;
176     private boolean mShouldSuppressNotificationDot;
177     private boolean mShouldSuppressNotificationList;
178     private boolean mShouldSuppressPeek;
179     private int mDesiredHeight;
180     @DimenRes
181     private int mDesiredHeightResId;
182     private int mTaskId;
183 
184     /** for logging **/
185     @Nullable
186     private InstanceId mInstanceId;
187     @Nullable
188     private String mChannelId;
189     private int mNotificationId;
190     private int mAppUid = -1;
191 
192     /**
193      * A bubble is created and can be updated. This intent is updated until the user first
194      * expands the bubble. Once the user has expanded the contents, we ignore the intent updates
195      * to prevent restarting the intent & possibly altering UI state in the activity in front of
196      * the user.
197      *
198      * Once the bubble is overflowed, the activity is finished and updates to the
199      * notification are respected. Typically an update to an overflowed bubble would result in
200      * that bubble being added back to the stack anyways.
201      */
202     @Nullable
203     private PendingIntent mPendingIntent;
204     private boolean mPendingIntentActive;
205     @Nullable
206     private PendingIntent.CancelListener mPendingIntentCancelListener;
207 
208     /**
209      * Sent when the bubble & notification are no longer visible to the user (i.e. no
210      * notification in the shade, no bubble in the stack or overflow).
211      */
212     @Nullable
213     private PendingIntent mDeleteIntent;
214 
215     /**
216      * Used for app & note bubbles.
217      */
218     @Nullable
219     private Intent mIntent;
220 
221     /**
222      * Set while preparing a transition for animation. Several steps are needed before animation
223      * starts, so this is used to detect and route associated events to the coordinating transition.
224      */
225     @Nullable
226     private BubbleTransitions.BubbleTransition mPreparingTransition;
227 
228     /**
229      * Create a bubble with limited information based on given {@link ShortcutInfo}.
230      * Note: Currently this is only being used when the bubble is persisted to disk.
231      */
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, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor, final Bubbles.BubbleMetadataFlagListener listener)232     public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
233             final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
234             int taskId, @Nullable final String locus, boolean isDismissable,
235             @ShellMainThread Executor mainExecutor,
236             @ShellBackgroundThread Executor bgExecutor,
237             final Bubbles.BubbleMetadataFlagListener listener) {
238         Objects.requireNonNull(key);
239         Objects.requireNonNull(shortcutInfo);
240         mMetadataShortcutId = shortcutInfo.getId();
241         mShortcutInfo = shortcutInfo;
242         mKey = key;
243         mGroupKey = null;
244         mLocusId = locus != null ? new LocusId(locus) : null;
245         mIsDismissable = isDismissable;
246         mFlags = 0;
247         mUser = shortcutInfo.getUserHandle();
248         mPackageName = shortcutInfo.getPackage();
249         mIcon = shortcutInfo.getIcon();
250         mDesiredHeight = desiredHeight;
251         mDesiredHeightResId = desiredHeightResId;
252         mTitle = title;
253         mShowBubbleUpdateDot = false;
254         mMainExecutor = mainExecutor;
255         mBgExecutor = bgExecutor;
256         mTaskId = taskId;
257         mBubbleMetadataFlagListener = listener;
258         // TODO (b/394085999) read/write type to xml
259         mType = BubbleType.TYPE_CHAT;
260     }
261 
Bubble( Intent intent, UserHandle user, @Nullable Icon icon, BubbleType type, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)262     private Bubble(
263             Intent intent,
264             UserHandle user,
265             @Nullable Icon icon,
266             BubbleType type,
267             String key,
268             @ShellMainThread Executor mainExecutor,
269             @ShellBackgroundThread Executor bgExecutor) {
270         mGroupKey = null;
271         mLocusId = null;
272         mFlags = 0;
273         mUser = user;
274         mIcon = icon;
275         mType = type;
276         mKey = key;
277         mShowBubbleUpdateDot = false;
278         mMainExecutor = mainExecutor;
279         mBgExecutor = bgExecutor;
280         mTaskId = INVALID_TASK_ID;
281         mIntent = intent;
282         mDesiredHeight = Integer.MAX_VALUE;
283         mPackageName = intent.getPackage();
284     }
285 
Bubble( PendingIntent intent, UserHandle user, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)286     private Bubble(
287             PendingIntent intent,
288             UserHandle user,
289             String key,
290             @ShellMainThread Executor mainExecutor,
291             @ShellBackgroundThread Executor bgExecutor) {
292         mGroupKey = null;
293         mLocusId = null;
294         mFlags = 0;
295         mUser = user;
296         mIcon = null;
297         mType = BubbleType.TYPE_APP;
298         mKey = key;
299         mShowBubbleUpdateDot = false;
300         mMainExecutor = mainExecutor;
301         mBgExecutor = bgExecutor;
302         mTaskId = INVALID_TASK_ID;
303         mPendingIntent = intent;
304         mIntent = null;
305         mDesiredHeight = Integer.MAX_VALUE;
306         mPackageName = ComponentUtils.getPackageName(intent);
307     }
308 
Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)309     private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor,
310             @ShellBackgroundThread Executor bgExecutor) {
311         mGroupKey = null;
312         mLocusId = null;
313         mFlags = 0;
314         mUser = info.getUserHandle();
315         mIcon = info.getIcon();
316         mType = BubbleType.TYPE_SHORTCUT;
317         mKey = getBubbleKeyForShortcut(info);
318         mShowBubbleUpdateDot = false;
319         mMainExecutor = mainExecutor;
320         mBgExecutor = bgExecutor;
321         mTaskId = INVALID_TASK_ID;
322         mIntent = null;
323         mDesiredHeight = Integer.MAX_VALUE;
324         mPackageName = info.getPackage();
325         mShortcutInfo = info;
326     }
327 
Bubble( TaskInfo task, UserHandle user, @Nullable Icon icon, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)328     private Bubble(
329             TaskInfo task,
330             UserHandle user,
331             @Nullable Icon icon,
332             String key,
333             @ShellMainThread Executor mainExecutor,
334             @ShellBackgroundThread Executor bgExecutor) {
335         mGroupKey = null;
336         mLocusId = null;
337         mFlags = 0;
338         mUser = user;
339         mIcon = icon;
340         mType = BubbleType.TYPE_APP;
341         mKey = key;
342         mShowBubbleUpdateDot = false;
343         mMainExecutor = mainExecutor;
344         mBgExecutor = bgExecutor;
345         mTaskId = task.taskId;
346         mIntent = null;
347         mDesiredHeight = Integer.MAX_VALUE;
348         mPackageName = task.baseActivity.getPackageName();
349     }
350 
351     /** Creates a note taking bubble. */
createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)352     public static Bubble createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon,
353             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
354         return new Bubble(intent,
355                 user,
356                 icon,
357                 BubbleType.TYPE_NOTE,
358                 getNoteBubbleKeyForApp(intent.getPackage(), user),
359                 mainExecutor, bgExecutor);
360     }
361 
362     /** Creates an app bubble. */
createAppBubble(PendingIntent intent, UserHandle user, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)363     public static Bubble createAppBubble(PendingIntent intent, UserHandle user,
364             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
365         return new Bubble(intent,
366                 user,
367                 /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
368                 mainExecutor, bgExecutor);
369     }
370 
371     /** Creates an app bubble. */
createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)372     public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
373             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
374         return new Bubble(intent,
375                 user,
376                 icon,
377                 BubbleType.TYPE_APP,
378                 getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
379                 mainExecutor, bgExecutor);
380     }
381 
382     /** Creates a task bubble. */
createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)383     public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon,
384             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
385         return new Bubble(info,
386                 user,
387                 icon,
388                 getAppBubbleKeyForTask(info),
389                 mainExecutor, bgExecutor);
390     }
391 
392     /** Creates a shortcut bubble. */
createShortcutBubble( ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)393     public static Bubble createShortcutBubble(
394             ShortcutInfo info,
395             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
396         return new Bubble(info, mainExecutor, bgExecutor);
397     }
398 
399     /**
400      * Returns the key for an app bubble from an app with package name, {@code packageName} on an
401      * Android user, {@code user}.
402      */
getAppBubbleKeyForApp(String packageName, UserHandle user)403     public static String getAppBubbleKeyForApp(String packageName, UserHandle user) {
404         Objects.requireNonNull(packageName);
405         Objects.requireNonNull(user);
406         return KEY_APP_BUBBLE + ":" + user.getIdentifier()  + ":" + packageName;
407     }
408 
409     /**
410      * Returns the key for a note bubble from an app with package name, {@code packageName} on an
411      * Android user, {@code user}.
412      */
getNoteBubbleKeyForApp(String packageName, UserHandle user)413     public static String getNoteBubbleKeyForApp(String packageName, UserHandle user) {
414         Objects.requireNonNull(packageName);
415         Objects.requireNonNull(user);
416         return KEY_NOTE_BUBBLE + ":" + user.getIdentifier()  + ":" + packageName;
417     }
418 
419     /**
420      * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the
421      * {@code shortcutInfo} id.
422      */
getBubbleKeyForShortcut(ShortcutInfo info)423     public static String getBubbleKeyForShortcut(ShortcutInfo info) {
424         return info.getPackage() + ":" + info.getUserId() + ":" + info.getId();
425     }
426 
427     /**
428      * Returns the key for an app bubble from an app with package name, {@code packageName} on an
429      * Android user, {@code user}.
430      */
getAppBubbleKeyForTask(TaskInfo taskInfo)431     public static String getAppBubbleKeyForTask(TaskInfo taskInfo) {
432         Objects.requireNonNull(taskInfo);
433         return KEY_APP_BUBBLE + ":" + taskInfo.taskId;
434     }
435 
436     /**
437      * Creates a chat bubble based on a notification (contents of {@link BubbleEntry}.
438      */
439     @VisibleForTesting(visibility = PRIVATE)
Bubble(@onNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor)440     public Bubble(@NonNull final BubbleEntry entry,
441             final Bubbles.BubbleMetadataFlagListener listener,
442             final Bubbles.PendingIntentCanceledListener intentCancelListener,
443             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
444         mType = BubbleType.TYPE_CHAT;
445         mKey = entry.getKey();
446         mGroupKey = entry.getGroupKey();
447         mLocusId = entry.getLocusId();
448         mBubbleMetadataFlagListener = listener;
449         mPendingIntentCancelListener = intent -> {
450             if (mPendingIntent != null) {
451                 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
452             }
453             mainExecutor.execute(() -> {
454                 intentCancelListener.onPendingIntentCanceled(this);
455             });
456         };
457         mMainExecutor = mainExecutor;
458         mBgExecutor = bgExecutor;
459         mTaskId = INVALID_TASK_ID;
460         setEntry(entry);
461     }
462 
463     /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */
asBubbleBarBubble()464     public BubbleInfo asBubbleBarBubble() {
465         return new BubbleInfo(getKey(),
466                 getFlags(),
467                 getShortcutId(),
468                 getIcon(),
469                 getUser().getIdentifier(),
470                 getPackageName(),
471                 getTitle(),
472                 getAppName(),
473                 isImportantConversation(),
474                 showAppBadge(),
475                 getParcelableFlyoutMessage());
476     }
477 
478     /** Creates a parcelable flyout message to send to launcher. */
479     @Nullable
getParcelableFlyoutMessage()480     private ParcelableFlyoutMessage getParcelableFlyoutMessage() {
481         if (mFlyoutMessage == null) {
482             return null;
483         }
484         // the icon is only used in group chats
485         Icon icon = mFlyoutMessage.isGroupChat ? mFlyoutMessage.senderIcon : null;
486         String title =
487                 mFlyoutMessage.senderName == null ? null : mFlyoutMessage.senderName.toString();
488         String message = mFlyoutMessage.message == null ? null : mFlyoutMessage.message.toString();
489         return new ParcelableFlyoutMessage(icon, title, message);
490     }
491 
492     @Override
getKey()493     public String getKey() {
494         return mKey;
495     }
496 
497     @Hide
isDismissable()498     public boolean isDismissable() {
499         return mIsDismissable;
500     }
501 
502     /**
503      * @see StatusBarNotification#getGroupKey()
504      * @return the group key for this bubble, if one exists.
505      */
getGroupKey()506     public String getGroupKey() {
507         return mGroupKey;
508     }
509 
getLocusId()510     public LocusId getLocusId() {
511         return mLocusId;
512     }
513 
getUser()514     public UserHandle getUser() {
515         return mUser;
516     }
517 
518     @NonNull
getPackageName()519     public String getPackageName() {
520         return mPackageName;
521     }
522 
523     @Override
getBubbleIcon()524     public Bitmap getBubbleIcon() {
525         return mBubbleBitmap;
526     }
527 
528     @Override
getAppBadge()529     public Bitmap getAppBadge() {
530         return mBadgeBitmap;
531     }
532 
533     @Override
getRawAppBadge()534     public Bitmap getRawAppBadge() {
535         return mRawBadgeBitmap;
536     }
537 
538     @Override
getDotColor()539     public int getDotColor() {
540         return mDotColor;
541     }
542 
543     @Override
getDotPath()544     public Path getDotPath() {
545         return mDotPath;
546     }
547 
548     @Nullable
getAppName()549     public String getAppName() {
550         return mAppName;
551     }
552 
553     @Nullable
getShortcutInfo()554     public ShortcutInfo getShortcutInfo() {
555         return mShortcutInfo;
556     }
557 
558     @Nullable
559     @Override
getIconView()560     public BadgedImageView getIconView() {
561         return mIconView;
562     }
563 
564     @Nullable
565     @Override
getExpandedView()566     public BubbleExpandedView getExpandedView() {
567         return mExpandedView;
568     }
569 
570     @Nullable
571     @Override
getBubbleBarExpandedView()572     public BubbleBarExpandedView getBubbleBarExpandedView() {
573         return mBubbleBarExpandedView;
574     }
575 
576     @Nullable
getTitle()577     public String getTitle() {
578         return mTitle;
579     }
580 
581     /**
582      * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new
583      * instance of {@link BubbleTaskView} is created.
584      */
getOrCreateBubbleTaskView(BubbleTaskViewFactory taskViewFactory)585     public BubbleTaskView getOrCreateBubbleTaskView(BubbleTaskViewFactory taskViewFactory) {
586         if (mBubbleTaskView == null) {
587             mBubbleTaskView = taskViewFactory.create();
588         }
589         return mBubbleTaskView;
590     }
591 
getTaskView()592     public TaskView getTaskView() {
593         return mBubbleTaskView.getTaskView();
594     }
595 
596     /**
597      * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
598      */
getShortcutId()599     String getShortcutId() {
600         return getShortcutInfo() != null
601                 ? getShortcutInfo().getId()
602                 : getMetadataShortcutId();
603     }
604 
getMetadataShortcutId()605     String getMetadataShortcutId() {
606         return mMetadataShortcutId;
607     }
608 
hasMetadataShortcutId()609     boolean hasMetadataShortcutId() {
610         return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty());
611     }
612 
getPreparingTransition()613     public BubbleTransitions.BubbleTransition getPreparingTransition() {
614         return mPreparingTransition;
615     }
616 
617     /**
618      * Call this to clean up the task for the bubble. Ensure this is always called when done with
619      * the bubble.
620      */
cleanupExpandedView()621     void cleanupExpandedView() {
622         cleanupExpandedView(true);
623     }
624 
cleanupExpandedView(boolean cleanupTaskView)625     private void cleanupExpandedView(boolean cleanupTaskView) {
626         if (mExpandedView != null) {
627             mExpandedView.cleanUpExpandedState();
628             mExpandedView = null;
629         }
630         if (mBubbleBarExpandedView != null) {
631             mBubbleBarExpandedView.cleanUpExpandedState();
632             mBubbleBarExpandedView = null;
633         }
634         if (cleanupTaskView) {
635             cleanupTaskView();
636         }
637         if (mPendingIntent != null) {
638             mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
639         }
640         mPendingIntentActive = false;
641     }
642 
643     /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */
cleanupTaskView()644     public void cleanupTaskView() {
645         if (mBubbleTaskView != null) {
646             mBubbleTaskView.cleanup();
647             mBubbleTaskView = null;
648         }
649     }
650 
651     /**
652      * Call when all the views should be removed/cleaned up.
653      */
cleanupViews()654     public void cleanupViews() {
655         ProtoLog.d(WM_SHELL_BUBBLES, "Bubble#cleanupViews=%s", getKey());
656         cleanupViews(true);
657     }
658 
659     /**
660      * Call when all the views should be removed/cleaned up.
661      *
662      * <p>If we're switching between bar and floating modes, pass {@code false} on
663      * {@code cleanupTaskView} to avoid recreating it in the new mode.
664      */
cleanupViews(boolean cleanupTaskView)665     public void cleanupViews(boolean cleanupTaskView) {
666         cleanupExpandedView(cleanupTaskView);
667         mIconView = null;
668     }
669 
setPendingIntentCanceled()670     void setPendingIntentCanceled() {
671         mPendingIntentCanceled = true;
672     }
673 
getPendingIntentCanceled()674     boolean getPendingIntentCanceled() {
675         return mPendingIntentCanceled;
676     }
677 
678     /**
679      * Sets whether to perform inflation on the same thread as the caller. This method should only
680      * be used in tests, not in production.
681      */
682     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)683     void setInflateSynchronously(boolean inflateSynchronously) {
684         mInflateSynchronously = inflateSynchronously;
685     }
686 
687     /**
688      * Sets the current bubble-transition that is coordinating a change in this bubble.
689      */
setPreparingTransition(BubbleTransitions.BubbleTransition transit)690     void setPreparingTransition(BubbleTransitions.BubbleTransition transit) {
691         mPreparingTransition = transit;
692     }
693 
694     /**
695      * Sets whether this bubble is considered text changed. This method is purely for
696      * testing.
697      */
698     @VisibleForTesting
setTextChangedForTest(boolean textChanged)699     void setTextChangedForTest(boolean textChanged) {
700         mIsTextChanged = textChanged;
701     }
702 
703     /**
704      * Starts a task to inflate & load any necessary information to display a bubble.
705      *
706      * @param callback the callback to notify one the bubble is ready to be displayed.
707      * @param context the context for the bubble.
708      * @param expandedViewManager the bubble expanded view manager.
709      * @param taskViewFactory the task view factory used to create the task view for the bubble.
710      * @param positioner the bubble positioner.
711      * @param stackView the view the bubble is added to, iff showing as floating.
712      * @param layerView the layer the bubble is added to, iff showing in the bubble bar.
713      * @param iconFactory the icon factory used to create images for the bubble.
714      */
inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean skipInflation)715     void inflate(BubbleViewInfoTask.Callback callback,
716             Context context,
717             BubbleExpandedViewManager expandedViewManager,
718             BubbleTaskViewFactory taskViewFactory,
719             BubblePositioner positioner,
720             @Nullable BubbleStackView stackView,
721             @Nullable BubbleBarLayerView layerView,
722             BubbleIconFactory iconFactory,
723             boolean skipInflation) {
724         ProtoLog.v(WM_SHELL_BUBBLES, "Inflate bubble key=%s", getKey());
725         if (Flags.bubbleViewInfoExecutors()) {
726             if (mInflationTask != null && !mInflationTask.isFinished()) {
727                 mInflationTask.cancel();
728             }
729             mInflationTask = new BubbleViewInfoTask(this,
730                     context,
731                     expandedViewManager,
732                     taskViewFactory,
733                     positioner,
734                     stackView,
735                     layerView,
736                     iconFactory,
737                     skipInflation,
738                     callback,
739                     mMainExecutor,
740                     mBgExecutor);
741             if (mInflateSynchronously) {
742                 mInflationTask.startSync();
743             } else {
744                 mInflationTask.start();
745             }
746         } else {
747             if (mInflationTaskLegacy != null && mInflationTaskLegacy.getStatus() != FINISHED) {
748                 mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */);
749             }
750             mInflationTaskLegacy = new BubbleViewInfoTaskLegacy(this,
751                     context,
752                     expandedViewManager,
753                     taskViewFactory,
754                     positioner,
755                     stackView,
756                     layerView,
757                     iconFactory,
758                     skipInflation,
759                     bubble -> {
760                         if (callback != null) {
761                             callback.onBubbleViewsReady(bubble);
762                         }
763                     },
764                     mMainExecutor,
765                     mBgExecutor);
766             if (mInflateSynchronously) {
767                 mInflationTaskLegacy.onPostExecute(mInflationTaskLegacy.doInBackground());
768             } else {
769                 mInflationTaskLegacy.execute();
770             }
771         }
772     }
773 
isInflated()774     boolean isInflated() {
775         return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null;
776     }
777 
stopInflation()778     void stopInflation() {
779         if (Flags.bubbleViewInfoExecutors()) {
780             if (mInflationTask == null) {
781                 return;
782             }
783             mInflationTask.cancel();
784         } else {
785             if (mInflationTaskLegacy == null) {
786                 return;
787             }
788             mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */);
789         }
790     }
791 
setViewInfo(BubbleViewInfoTask.BubbleViewInfo info)792     void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) {
793         if (!isInflated()) {
794             mIconView = info.imageView;
795             mExpandedView = info.expandedView;
796             mBubbleBarExpandedView = info.bubbleBarExpandedView;
797         }
798 
799         mShortcutInfo = info.shortcutInfo;
800         mAppName = info.appName;
801         if (mTitle == null) {
802             mTitle = mAppName;
803         }
804         mFlyoutMessage = info.flyoutMessage;
805 
806         mBadgeBitmap = info.badgeBitmap;
807         mRawBadgeBitmap = info.rawBadgeBitmap;
808         mBubbleBitmap = info.bubbleBitmap;
809 
810         mDotColor = info.dotColor;
811         mDotPath = info.dotPath;
812 
813         if (mExpandedView != null) {
814             mExpandedView.update(this /* bubble */);
815         }
816         if (mBubbleBarExpandedView != null) {
817             mBubbleBarExpandedView.update(this /* bubble */);
818         }
819         if (mIconView != null) {
820             mIconView.setRenderedBubble(this /* bubble */);
821         }
822     }
823 
824     /**
825      * @deprecated {@link BubbleViewInfoTaskLegacy} is deprecated.
826      */
827     @Deprecated
setViewInfoLegacy(BubbleViewInfoTaskLegacy.BubbleViewInfo info)828     void setViewInfoLegacy(BubbleViewInfoTaskLegacy.BubbleViewInfo info) {
829         if (!isInflated()) {
830             mIconView = info.imageView;
831             mExpandedView = info.expandedView;
832             mBubbleBarExpandedView = info.bubbleBarExpandedView;
833         }
834 
835         mShortcutInfo = info.shortcutInfo;
836         mAppName = info.appName;
837         if (mTitle == null) {
838             mTitle = mAppName;
839         }
840         mFlyoutMessage = info.flyoutMessage;
841 
842         mBadgeBitmap = info.badgeBitmap;
843         mRawBadgeBitmap = info.rawBadgeBitmap;
844         mBubbleBitmap = info.bubbleBitmap;
845 
846         mDotColor = info.dotColor;
847         mDotPath = info.dotPath;
848 
849         if (mExpandedView != null) {
850             mExpandedView.update(this /* bubble */);
851         }
852         if (mBubbleBarExpandedView != null) {
853             mBubbleBarExpandedView.update(this /* bubble */);
854         }
855         if (mIconView != null) {
856             mIconView.setRenderedBubble(this /* bubble */);
857         }
858     }
859 
860     /**
861      * Set visibility of bubble in the expanded state.
862      *
863      * <p>Note that this contents visibility doesn't affect visibility at {@link android.view.View},
864      * and setting {@code false} actually means rendering the expanded view in transparent.
865      *
866      * @param visibility {@code true} if the expanded bubble should be visible on the screen.
867      */
868     @Override
setTaskViewVisibility(boolean visibility)869     public void setTaskViewVisibility(boolean visibility) {
870         if (mExpandedView != null) {
871             mExpandedView.setContentVisibility(visibility);
872         }
873     }
874 
875     /**
876      * Sets the entry associated with this bubble.
877      */
setEntry(@onNull final BubbleEntry entry)878     void setEntry(@NonNull final BubbleEntry entry) {
879         Objects.requireNonNull(entry);
880         boolean showingDotPreviously = showDot();
881         mLastUpdated = entry.getStatusBarNotification().getPostTime();
882         mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification();
883         mPackageName = entry.getStatusBarNotification().getPackageName();
884         mUser = entry.getStatusBarNotification().getUser();
885         mTitle = getTitle(entry);
886         mChannelId = entry.getStatusBarNotification().getNotification().getChannelId();
887         mNotificationId = entry.getStatusBarNotification().getId();
888         mAppUid = entry.getStatusBarNotification().getUid();
889         mInstanceId = entry.getStatusBarNotification().getInstanceId();
890         mFlyoutMessage = extractFlyoutMessage(entry);
891         if (entry.getRanking() != null) {
892             mShortcutInfo = entry.getRanking().getConversationShortcutInfo();
893             mIsTextChanged = entry.getRanking().isTextChanged();
894             if (entry.getRanking().getChannel() != null) {
895                 mIsImportantConversation =
896                         entry.getRanking().getChannel().isImportantConversation();
897             }
898         }
899         if (entry.getBubbleMetadata() != null) {
900             mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId();
901             mFlags = entry.getBubbleMetadata().getFlags();
902             mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight();
903             mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId();
904             mIcon = entry.getBubbleMetadata().getIcon();
905 
906             if (!mPendingIntentActive || mPendingIntent == null) {
907                 if (mPendingIntent != null) {
908                     mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
909                 }
910                 mPendingIntent = entry.getBubbleMetadata().getIntent();
911                 if (mPendingIntent != null) {
912                     mPendingIntent.registerCancelListener(mPendingIntentCancelListener);
913                 }
914             } else if (mPendingIntent != null && entry.getBubbleMetadata().getIntent() == null) {
915                 // Was an intent bubble now it's a shortcut bubble... still unregister the listener
916                 mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
917                 mPendingIntentActive = false;
918                 mPendingIntent = null;
919             }
920             mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
921         }
922 
923         mIsDismissable = entry.isDismissable();
924         mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
925         mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
926         mShouldSuppressPeek = entry.shouldSuppressPeek();
927         if (showingDotPreviously != showDot()) {
928             // This will update the UI if needed
929             setShowDot(showDot());
930         }
931     }
932 
933     /**
934      * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles
935      * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the
936      * icon from the shortcut.
937      */
938     @Nullable
getIcon()939     public Icon getIcon() {
940         return mIcon;
941     }
942 
isTextChanged()943     boolean isTextChanged() {
944         return mIsTextChanged;
945     }
946 
947     /**
948      * @return the last time this bubble was updated or accessed, whichever is most recent.
949      */
getLastActivity()950     long getLastActivity() {
951         return Math.max(mLastUpdated, mLastAccessed);
952     }
953 
954     /**
955      * Sets if the intent used for this bubble is currently active (i.e. populating an
956      * expanded view, expanded or not).
957      */
setPendingIntentActive()958     void setPendingIntentActive() {
959         mPendingIntentActive = true;
960     }
961 
962     /**
963      * Whether the pending intent of this bubble is active (i.e. has been sent).
964      */
isPendingIntentActive()965     boolean isPendingIntentActive() {
966         return mPendingIntentActive;
967     }
968 
getInstanceId()969     public InstanceId getInstanceId() {
970         return mInstanceId;
971     }
972 
973     @Nullable
getChannelId()974     public String getChannelId() {
975         return mChannelId;
976     }
977 
getNotificationId()978     public int getNotificationId() {
979         return mNotificationId;
980     }
981 
982     /**
983      * @return the task id of the task in which bubble contents is drawn.
984      */
985     @Override
getTaskId()986     public int getTaskId() {
987         if (mBubbleBarExpandedView != null) {
988             return mBubbleBarExpandedView.getTaskId();
989         }
990         return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId;
991     }
992 
993     /**
994      * Should be invoked whenever a Bubble is accessed (selected while expanded).
995      */
markAsAccessedAt(long lastAccessedMillis)996     void markAsAccessedAt(long lastAccessedMillis) {
997         mLastAccessed = lastAccessedMillis;
998         setSuppressNotification(true);
999         setShowDot(false /* show */);
1000     }
1001 
1002     /**
1003      * Should be invoked whenever a Bubble is promoted from overflow.
1004      */
markUpdatedAt(long lastAccessedMillis)1005     void markUpdatedAt(long lastAccessedMillis) {
1006         mLastUpdated = lastAccessedMillis;
1007     }
1008 
1009     /**
1010      * Whether this notification should be shown in the shade.
1011      */
showInShade()1012     boolean showInShade() {
1013         return !shouldSuppressNotification() || !mIsDismissable;
1014     }
1015 
1016     /**
1017      * Whether this bubble is currently being hidden from the stack.
1018      */
isSuppressed()1019     boolean isSuppressed() {
1020         return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0;
1021     }
1022 
1023     /**
1024      * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API to
1025      * hide the bubble when in the same content).
1026      */
isSuppressable()1027     boolean isSuppressable() {
1028         return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0;
1029     }
1030 
1031     /**
1032      * Whether this notification conversation is important.
1033      */
isImportantConversation()1034     boolean isImportantConversation() {
1035         return mIsImportantConversation;
1036     }
1037 
1038     /**
1039      * Sets whether this notification should be suppressed in the shade.
1040      */
1041     @VisibleForTesting
setSuppressNotification(boolean suppressNotification)1042     public void setSuppressNotification(boolean suppressNotification) {
1043         boolean prevShowInShade = showInShade();
1044         if (suppressNotification) {
1045             mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1046         } else {
1047             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1048         }
1049 
1050         if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) {
1051             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
1052         }
1053     }
1054 
1055     /**
1056      * Sets whether this bubble should be suppressed from the stack.
1057      */
setSuppressBubble(boolean suppressBubble)1058     public void setSuppressBubble(boolean suppressBubble) {
1059         if (!isSuppressable()) {
1060             Log.e(TAG, "calling setSuppressBubble on "
1061                     + getKey() + " when bubble not suppressable");
1062             return;
1063         }
1064         boolean prevSuppressed = isSuppressed();
1065         if (suppressBubble) {
1066             mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
1067         } else {
1068             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
1069         }
1070         if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) {
1071             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
1072         }
1073     }
1074 
1075     /**
1076      * Sets whether the bubble for this notification should show a dot indicating updated content.
1077      */
setShowDot(boolean showDot)1078     void setShowDot(boolean showDot) {
1079         mShowBubbleUpdateDot = showDot;
1080 
1081         if (mIconView != null) {
1082             mIconView.updateDotVisibility(true /* animate */);
1083         }
1084     }
1085 
1086     /**
1087      * Whether the bubble for this notification should show a dot indicating updated content.
1088      */
1089     @Override
showDot()1090     public boolean showDot() {
1091         return mShowBubbleUpdateDot
1092                 && !mShouldSuppressNotificationDot
1093                 && !shouldSuppressNotification();
1094     }
1095 
1096     /**
1097      * Whether the flyout for the bubble should be shown.
1098      */
1099     @VisibleForTesting
showFlyout()1100     public boolean showFlyout() {
1101         return !mSuppressFlyout && !mShouldSuppressPeek
1102                 && !shouldSuppressNotification()
1103                 && !mShouldSuppressNotificationList;
1104     }
1105 
1106     /**
1107      * Set whether the flyout text for the bubble should be shown when an update is received.
1108      *
1109      * @param suppressFlyout whether the flyout text is shown
1110      */
setSuppressFlyout(boolean suppressFlyout)1111     void setSuppressFlyout(boolean suppressFlyout) {
1112         mSuppressFlyout = suppressFlyout;
1113     }
1114 
getFlyoutMessage()1115     FlyoutMessage getFlyoutMessage() {
1116         return mFlyoutMessage;
1117     }
1118 
getRawDesiredHeight()1119     int getRawDesiredHeight() {
1120         return mDesiredHeight;
1121     }
1122 
getRawDesiredHeightResId()1123     int getRawDesiredHeightResId() {
1124         return mDesiredHeightResId;
1125     }
1126 
getDesiredHeight(Context context)1127     float getDesiredHeight(Context context) {
1128         boolean useRes = mDesiredHeightResId != 0;
1129         if (useRes) {
1130             return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName,
1131                     mUser.getIdentifier());
1132         } else {
1133             return mDesiredHeight * context.getResources().getDisplayMetrics().density;
1134         }
1135     }
1136 
getDesiredHeightString()1137     String getDesiredHeightString() {
1138         boolean useRes = mDesiredHeightResId != 0;
1139         if (useRes) {
1140             return String.valueOf(mDesiredHeightResId);
1141         } else {
1142             return String.valueOf(mDesiredHeight);
1143         }
1144     }
1145 
1146     /**
1147      * Returns the pending intent used to populate the bubble.
1148      */
1149     @Nullable
getPendingIntent()1150     PendingIntent getPendingIntent() {
1151         return mPendingIntent;
1152     }
1153 
1154     /**
1155      * Whether an app badge should be shown for this bubble.
1156      */
showAppBadge()1157     public boolean showAppBadge() {
1158         return isChat() || isShortcut() || isNote();
1159     }
1160 
1161     /**
1162      * Returns the pending intent to send when a bubble is dismissed (set via the notification API).
1163      */
1164     @Nullable
getDeleteIntent()1165     PendingIntent getDeleteIntent() {
1166         return mDeleteIntent;
1167     }
1168 
1169     /**
1170      * Returns the intent used to populate the bubble.
1171      */
1172     @Nullable
getIntent()1173     public Intent getIntent() {
1174         return mIntent;
1175     }
1176 
1177     /**
1178      * Sets the intent used to populate the bubble.
1179      */
setIntent(Intent intent)1180     void setIntent(Intent intent) {
1181         mIntent = intent;
1182     }
1183 
1184     /**
1185      * Returns whether this bubble is a conversation from the notification API.
1186      */
isChat()1187     public boolean isChat() {
1188         return mType == BubbleType.TYPE_CHAT;
1189     }
1190 
1191     /**
1192      * Returns whether this bubble is a note from the note taking API.
1193      */
isNote()1194     public boolean isNote() {
1195         return mType == BubbleType.TYPE_NOTE;
1196     }
1197 
1198     /**
1199      * Returns whether this bubble is a shortcut.
1200      */
isShortcut()1201     public boolean isShortcut() {
1202         return mType == BubbleType.TYPE_SHORTCUT;
1203     }
1204 
1205     /**
1206      * Returns whether this bubble is an app.
1207      */
isApp()1208     public boolean isApp() {
1209         return mType == BubbleType.TYPE_APP;
1210     }
1211 
1212     /** Creates open app settings intent */
getSettingsIntent(final Context context)1213     public Intent getSettingsIntent(final Context context) {
1214         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
1215         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
1216         final int uid = getUid(context);
1217         if (uid != -1) {
1218             intent.putExtra(Settings.EXTRA_APP_UID, uid);
1219         }
1220         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1221         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1222         return intent;
1223     }
1224 
getAppUid()1225     public int getAppUid() {
1226         return mAppUid;
1227     }
1228 
getUid(final Context context)1229     private int getUid(final Context context) {
1230         if (mAppUid != -1) return mAppUid;
1231         final PackageManager pm = BubbleController.getPackageManagerForUser(context,
1232                 mUser.getIdentifier());
1233         if (pm == null) return -1;
1234         try {
1235             final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0);
1236             return info.uid;
1237         } catch (PackageManager.NameNotFoundException e) {
1238             Log.e(TAG, "cannot find uid", e);
1239         }
1240         return -1;
1241     }
1242 
getDimenForPackageUser(Context context, int resId, String pkg, int userId)1243     private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
1244         Resources r;
1245         if (pkg != null) {
1246             try {
1247                 if (userId == UserHandle.USER_ALL) {
1248                     userId = UserHandle.USER_SYSTEM;
1249                 }
1250                 r = context.createContextAsUser(UserHandle.of(userId), /* flags */ 0)
1251                         .getPackageManager().getResourcesForApplication(pkg);
1252                 return r.getDimensionPixelSize(resId);
1253             } catch (PackageManager.NameNotFoundException ex) {
1254                 // Uninstalled, don't care
1255             } catch (Resources.NotFoundException e) {
1256                 // Invalid res id, return 0 and user our default
1257                 Log.e(TAG, "Couldn't find desired height res id", e);
1258             }
1259         }
1260         return 0;
1261     }
1262 
shouldSuppressNotification()1263     private boolean shouldSuppressNotification() {
1264         return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
1265     }
1266 
shouldAutoExpand()1267     public boolean shouldAutoExpand() {
1268         return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
1269     }
1270 
1271     @VisibleForTesting
setShouldAutoExpand(boolean shouldAutoExpand)1272     public void setShouldAutoExpand(boolean shouldAutoExpand) {
1273         boolean prevAutoExpand = shouldAutoExpand();
1274         if (shouldAutoExpand) {
1275             enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
1276         } else {
1277             disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
1278         }
1279         if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) {
1280             mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
1281         }
1282     }
1283 
setIsBubble(final boolean isBubble)1284     public void setIsBubble(final boolean isBubble) {
1285         mIsBubble = isBubble;
1286     }
1287 
isBubble()1288     public boolean isBubble() {
1289         return mIsBubble;
1290     }
1291 
enable(int option)1292     public void enable(int option) {
1293         mFlags |= option;
1294     }
1295 
disable(int option)1296     public void disable(int option) {
1297         mFlags &= ~option;
1298     }
1299 
isEnabled(int option)1300     public boolean isEnabled(int option) {
1301         return (mFlags & option) != 0;
1302     }
1303 
getFlags()1304     public int getFlags() {
1305         return mFlags;
1306     }
1307 
1308     @Override
toString()1309     public String toString() {
1310         return "Bubble{" + mKey + '}';
1311     }
1312 
1313     /**
1314      * Description of current bubble state.
1315      */
dump(@onNull PrintWriter pw)1316     public void dump(@NonNull PrintWriter pw) {
1317         pw.print("key: "); pw.println(mKey);
1318         pw.print("  showInShade:   "); pw.println(showInShade());
1319         pw.print("  showDot:       "); pw.println(showDot());
1320         pw.print("  showFlyout:    "); pw.println(showFlyout());
1321         pw.print("  lastActivity:  "); pw.println(getLastActivity());
1322         pw.print("  desiredHeight: "); pw.println(getDesiredHeightString());
1323         pw.print("  suppressNotif: "); pw.println(shouldSuppressNotification());
1324         pw.print("  autoExpand:    "); pw.println(shouldAutoExpand());
1325         pw.print("  isDismissable: "); pw.println(mIsDismissable);
1326         pw.println("  bubbleMetadataFlagListener null?: " + (mBubbleMetadataFlagListener == null));
1327         if (mExpandedView != null) {
1328             mExpandedView.dump(pw, "  ");
1329         }
1330     }
1331 
1332     @Override
equals(Object o)1333     public boolean equals(Object o) {
1334         if (this == o) return true;
1335         if (!(o instanceof Bubble)) return false;
1336         Bubble bubble = (Bubble) o;
1337         return Objects.equals(mKey, bubble.mKey);
1338     }
1339 
1340     @Override
hashCode()1341     public int hashCode() {
1342         return Objects.hash(mKey);
1343     }
1344 
1345     @Nullable
getTitle(@onNull final BubbleEntry e)1346     private static String getTitle(@NonNull final BubbleEntry e) {
1347         final CharSequence titleCharSeq = e.getStatusBarNotification()
1348                 .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
1349         return titleCharSeq == null ? null : titleCharSeq.toString();
1350     }
1351 
1352     /**
1353      * Returns our best guess for the most relevant text summary of the latest update to this
1354      * notification, based on its type. Returns null if there should not be an update message.
1355      */
1356     @NonNull
extractFlyoutMessage(BubbleEntry entry)1357     static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) {
1358         Objects.requireNonNull(entry);
1359         final Notification underlyingNotif = entry.getStatusBarNotification().getNotification();
1360         final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
1361 
1362         Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
1363         bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
1364                 Notification.EXTRA_IS_GROUP_CONVERSATION);
1365         try {
1366             if (Notification.BigTextStyle.class.equals(style)) {
1367                 // Return the big text, it is big so probably important. If it's not there use the
1368                 // normal text.
1369                 CharSequence bigText =
1370                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
1371                 bubbleMessage.message = !TextUtils.isEmpty(bigText)
1372                         ? bigText
1373                         : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
1374                 return bubbleMessage;
1375             } else if (Notification.MessagingStyle.class.equals(style)) {
1376                 final List<Notification.MessagingStyle.Message> messages =
1377                         Notification.MessagingStyle.Message.getMessagesFromBundleArray(
1378                                 (Parcelable[]) underlyingNotif.extras.get(
1379                                         Notification.EXTRA_MESSAGES));
1380 
1381                 final Notification.MessagingStyle.Message latestMessage =
1382                         Notification.MessagingStyle.findLatestIncomingMessage(messages);
1383                 if (latestMessage != null) {
1384                     bubbleMessage.message = latestMessage.getText();
1385                     Person sender = latestMessage.getSenderPerson();
1386                     bubbleMessage.senderName = sender != null ? sender.getName() : null;
1387                     bubbleMessage.senderAvatar = null;
1388                     bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null;
1389                     return bubbleMessage;
1390                 }
1391             } else if (Notification.InboxStyle.class.equals(style)) {
1392                 CharSequence[] lines =
1393                         underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
1394 
1395                 // Return the last line since it should be the most recent.
1396                 if (lines != null && lines.length > 0) {
1397                     bubbleMessage.message = lines[lines.length - 1];
1398                     return bubbleMessage;
1399                 }
1400             } else if (Notification.MediaStyle.class.equals(style)) {
1401                 // Return nothing, media updates aren't typically useful as a text update.
1402                 return bubbleMessage;
1403             } else {
1404                 // Default to text extra.
1405                 bubbleMessage.message =
1406                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
1407                 return bubbleMessage;
1408             }
1409         } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
1410             // No use crashing, we'll just return null and the caller will assume there's no update
1411             // message.
1412             e.printStackTrace();
1413         }
1414 
1415         return bubbleMessage;
1416     }
1417 }
1418