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