• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 
17 package com.android.systemui.bubbles;
18 
19 import static android.app.Notification.FLAG_BUBBLE;
20 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
21 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
22 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
23 import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
24 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
25 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
26 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
27 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
28 import static android.view.Display.DEFAULT_DISPLAY;
29 import static android.view.Display.INVALID_DISPLAY;
30 import static android.view.View.INVISIBLE;
31 import static android.view.View.VISIBLE;
32 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
33 
34 import static com.android.systemui.statusbar.StatusBarState.SHADE;
35 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON;
36 
37 import static java.lang.annotation.ElementType.FIELD;
38 import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
39 import static java.lang.annotation.ElementType.PARAMETER;
40 import static java.lang.annotation.RetentionPolicy.SOURCE;
41 
42 import android.app.ActivityManager;
43 import android.app.ActivityManager.RunningTaskInfo;
44 import android.app.Notification;
45 import android.app.NotificationManager;
46 import android.app.PendingIntent;
47 import android.content.Context;
48 import android.content.pm.ActivityInfo;
49 import android.content.pm.ParceledListSlice;
50 import android.content.res.Configuration;
51 import android.graphics.Rect;
52 import android.os.RemoteException;
53 import android.os.ServiceManager;
54 import android.provider.Settings;
55 import android.service.notification.NotificationListenerService.RankingMap;
56 import android.service.notification.StatusBarNotification;
57 import android.service.notification.ZenModeConfig;
58 import android.util.Log;
59 import android.util.Pair;
60 import android.view.Display;
61 import android.view.IPinnedStackController;
62 import android.view.IPinnedStackListener;
63 import android.view.ViewGroup;
64 import android.widget.FrameLayout;
65 
66 import androidx.annotation.IntDef;
67 import androidx.annotation.MainThread;
68 import androidx.annotation.Nullable;
69 
70 import com.android.internal.annotations.VisibleForTesting;
71 import com.android.internal.statusbar.IStatusBarService;
72 import com.android.systemui.Dependency;
73 import com.android.systemui.R;
74 import com.android.systemui.plugins.statusbar.StatusBarStateController;
75 import com.android.systemui.shared.system.ActivityManagerWrapper;
76 import com.android.systemui.shared.system.TaskStackChangeListener;
77 import com.android.systemui.shared.system.WindowManagerWrapper;
78 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
79 import com.android.systemui.statusbar.notification.NotificationEntryListener;
80 import com.android.systemui.statusbar.notification.NotificationEntryManager;
81 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
82 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
83 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
84 import com.android.systemui.statusbar.phone.StatusBarWindowController;
85 import com.android.systemui.statusbar.policy.ConfigurationController;
86 import com.android.systemui.statusbar.policy.ZenModeController;
87 
88 import java.lang.annotation.Retention;
89 import java.lang.annotation.Target;
90 import java.util.List;
91 
92 import javax.inject.Inject;
93 import javax.inject.Singleton;
94 
95 /**
96  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
97  * Bubbles can be expanded to show more content.
98  *
99  * The controller manages addition, removal, and visible state of bubbles on screen.
100  */
101 @Singleton
102 public class BubbleController implements ConfigurationController.ConfigurationListener {
103 
104     private static final String TAG = "BubbleController";
105     private static final boolean DEBUG = false;
106 
107     @Retention(SOURCE)
108     @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
109             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE})
110     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
111     @interface DismissReason {}
112 
113     static final int DISMISS_USER_GESTURE = 1;
114     static final int DISMISS_AGED = 2;
115     static final int DISMISS_TASK_FINISHED = 3;
116     static final int DISMISS_BLOCKED = 4;
117     static final int DISMISS_NOTIF_CANCEL = 5;
118     static final int DISMISS_ACCESSIBILITY_ACTION = 6;
119     static final int DISMISS_NO_LONGER_BUBBLE = 7;
120 
121     public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
122 
123     // Enables some subset of notifs to automatically become bubbles
124     public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
125 
126     /** Flag to enable or disable the entire feature */
127     private static final String ENABLE_BUBBLES = "experiment_enable_bubbles";
128     /** Auto bubble flags set whether different notif types should be presented as a bubble */
129     private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
130     private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
131     private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
132 
133     /** Use an activityView for an auto-bubbled notifs if it has an appropriate content intent */
134     private static final String ENABLE_BUBBLE_CONTENT_INTENT = "experiment_bubble_content_intent";
135 
136     private static final String BUBBLE_STIFFNESS = "experiment_bubble_stiffness";
137     private static final String BUBBLE_BOUNCINESS = "experiment_bubble_bounciness";
138 
139     private final Context mContext;
140     private final NotificationEntryManager mNotificationEntryManager;
141     private final BubbleTaskStackListener mTaskStackListener;
142     private BubbleStateChangeListener mStateChangeListener;
143     private BubbleExpandListener mExpandListener;
144     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
145 
146     private BubbleData mBubbleData;
147     @Nullable private BubbleStackView mStackView;
148 
149     // Bubbles get added to the status bar view
150     private final StatusBarWindowController mStatusBarWindowController;
151     private final ZenModeController mZenModeController;
152     private StatusBarStateListener mStatusBarStateListener;
153 
154     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
155     private IStatusBarService mBarService;
156 
157     // Used for determining view rect for touch interaction
158     private Rect mTempRect = new Rect();
159 
160     /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
161     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
162 
163     /**
164      * Listener to be notified when some states of the bubbles change.
165      */
166     public interface BubbleStateChangeListener {
167         /**
168          * Called when the stack has bubbles or no longer has bubbles.
169          */
onHasBubblesChanged(boolean hasBubbles)170         void onHasBubblesChanged(boolean hasBubbles);
171     }
172 
173     /**
174      * Listener to find out about stack expansion / collapse events.
175      */
176     public interface BubbleExpandListener {
177         /**
178          * Called when the expansion state of the bubble stack changes.
179          *
180          * @param isExpanding whether it's expanding or collapsing
181          * @param key the notification key associated with bubble being expanded
182          */
onBubbleExpandChanged(boolean isExpanding, String key)183         void onBubbleExpandChanged(boolean isExpanding, String key);
184     }
185 
186     /**
187      * Listens for the current state of the status bar and updates the visibility state
188      * of bubbles as needed.
189      */
190     private class StatusBarStateListener implements StatusBarStateController.StateListener {
191         private int mState;
192         /**
193          * Returns the current status bar state.
194          */
getCurrentState()195         public int getCurrentState() {
196             return mState;
197         }
198 
199         @Override
onStateChanged(int newState)200         public void onStateChanged(int newState) {
201             mState = newState;
202             boolean shouldCollapse = (mState != SHADE);
203             if (shouldCollapse) {
204                 collapseStack();
205             }
206             updateStack();
207         }
208     }
209 
210     @Inject
BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController)211     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
212             BubbleData data, ConfigurationController configurationController,
213             NotificationInterruptionStateProvider interruptionStateProvider,
214             ZenModeController zenModeController) {
215         this(context, statusBarWindowController, data, null /* synchronizer */,
216                 configurationController, interruptionStateProvider, zenModeController);
217     }
218 
BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController)219     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
220             BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
221             ConfigurationController configurationController,
222             NotificationInterruptionStateProvider interruptionStateProvider,
223             ZenModeController zenModeController) {
224         mContext = context;
225         mNotificationInterruptionStateProvider = interruptionStateProvider;
226         mZenModeController = zenModeController;
227         mZenModeController.addCallback(new ZenModeController.Callback() {
228             @Override
229             public void onZenChanged(int zen) {
230                 updateStackViewForZenConfig();
231             }
232 
233             @Override
234             public void onConfigChanged(ZenModeConfig config) {
235                 updateStackViewForZenConfig();
236             }
237         });
238 
239         configurationController.addCallback(this /* configurationListener */);
240 
241         mBubbleData = data;
242         mBubbleData.setListener(mBubbleDataListener);
243 
244         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
245         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
246         mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
247 
248         mStatusBarWindowController = statusBarWindowController;
249         mStatusBarStateListener = new StatusBarStateListener();
250         Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
251 
252         mTaskStackListener = new BubbleTaskStackListener();
253         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
254 
255         try {
256             WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener());
257         } catch (RemoteException e) {
258             e.printStackTrace();
259         }
260         mSurfaceSynchronizer = synchronizer;
261 
262         mBarService = IStatusBarService.Stub.asInterface(
263                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
264     }
265 
266     /**
267      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
268      * method initializes the stack view and adds it to the StatusBar just above the scrim.
269      */
ensureStackViewCreated()270     private void ensureStackViewCreated() {
271         if (mStackView == null) {
272             mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer);
273             ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
274             // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no
275             //  scrim between bubble and the shade
276             int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
277             sbv.addView(mStackView, bubblePosition,
278                     new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
279             if (mExpandListener != null) {
280                 mStackView.setExpandListener(mExpandListener);
281             }
282 
283             updateStackViewForZenConfig();
284         }
285     }
286 
287     @Override
onUiModeChanged()288     public void onUiModeChanged() {
289         if (mStackView != null) {
290             mStackView.onThemeChanged();
291         }
292     }
293 
294     @Override
onOverlayChanged()295     public void onOverlayChanged() {
296         if (mStackView != null) {
297             mStackView.onThemeChanged();
298         }
299     }
300 
301     @Override
onConfigChanged(Configuration newConfig)302     public void onConfigChanged(Configuration newConfig) {
303         if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) {
304             mStackView.onOrientationChanged();
305             mOrientation = newConfig.orientation;
306         }
307     }
308 
309     /**
310      * Set a listener to be notified when some states of the bubbles change.
311      */
setBubbleStateChangeListener(BubbleStateChangeListener listener)312     public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
313         mStateChangeListener = listener;
314     }
315 
316     /**
317      * Set a listener to be notified of bubble expand events.
318      */
setExpandListener(BubbleExpandListener listener)319     public void setExpandListener(BubbleExpandListener listener) {
320         mExpandListener = ((isExpanding, key) -> {
321             if (listener != null) {
322                 listener.onBubbleExpandChanged(isExpanding, key);
323             }
324             mStatusBarWindowController.setBubbleExpanded(isExpanding);
325         });
326         if (mStackView != null) {
327             mStackView.setExpandListener(mExpandListener);
328         }
329     }
330 
331     /**
332      * Whether or not there are bubbles present, regardless of them being visible on the
333      * screen (e.g. if on AOD).
334      */
hasBubbles()335     public boolean hasBubbles() {
336         if (mStackView == null) {
337             return false;
338         }
339         return mBubbleData.hasBubbles();
340     }
341 
342     /**
343      * Whether the stack of bubbles is expanded or not.
344      */
isStackExpanded()345     public boolean isStackExpanded() {
346         return mBubbleData.isExpanded();
347     }
348 
349     /**
350      * Tell the stack of bubbles to expand.
351      */
expandStack()352     public void expandStack() {
353         mBubbleData.setExpanded(true);
354     }
355 
356     /**
357      * Tell the stack of bubbles to collapse.
358      */
collapseStack()359     public void collapseStack() {
360         mBubbleData.setExpanded(false /* expanded */);
361     }
362 
selectBubble(Bubble bubble)363     void selectBubble(Bubble bubble) {
364         mBubbleData.setSelectedBubble(bubble);
365     }
366 
367     @VisibleForTesting
selectBubble(String key)368     void selectBubble(String key) {
369         Bubble bubble = mBubbleData.getBubbleWithKey(key);
370         selectBubble(bubble);
371     }
372 
373     /**
374      * Request the stack expand if needed, then select the specified Bubble as current.
375      *
376      * @param notificationKey the notification key for the bubble to be selected
377      */
expandStackAndSelectBubble(String notificationKey)378     public void expandStackAndSelectBubble(String notificationKey) {
379         Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey);
380         if (bubble != null) {
381             mBubbleData.setSelectedBubble(bubble);
382             mBubbleData.setExpanded(true);
383         }
384     }
385 
386     /**
387      * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
388      */
dismissStack(@ismissReason int reason)389     void dismissStack(@DismissReason int reason) {
390         mBubbleData.dismissAll(reason);
391     }
392 
393     /**
394      * Directs a back gesture at the bubble stack. When opened, the current expanded bubble
395      * is forwarded a back key down/up pair.
396      */
performBackPressIfNeeded()397     public void performBackPressIfNeeded() {
398         if (mStackView != null) {
399             mStackView.performBackPressIfNeeded();
400         }
401     }
402 
403     /**
404      * Adds or updates a bubble associated with the provided notification entry.
405      *
406      * @param notif the notification associated with this bubble.
407      */
updateBubble(NotificationEntry notif)408     void updateBubble(NotificationEntry notif) {
409         // If this is an interruptive notif, mark that it's interrupted
410         if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) {
411             notif.setInterruption();
412         }
413         mBubbleData.notificationEntryUpdated(notif);
414     }
415 
416     /**
417      * Removes the bubble associated with the {@param uri}.
418      * <p>
419      * Must be called from the main thread.
420      */
421     @MainThread
removeBubble(String key, int reason)422     void removeBubble(String key, int reason) {
423         // TEMP: refactor to change this to pass entry
424         Bubble bubble = mBubbleData.getBubbleWithKey(key);
425         if (bubble != null) {
426             mBubbleData.notificationEntryRemoved(bubble.entry, reason);
427         }
428     }
429 
430     @SuppressWarnings("FieldCanBeLocal")
431     private final NotificationRemoveInterceptor mRemoveInterceptor =
432             new NotificationRemoveInterceptor() {
433             @Override
434             public boolean onNotificationRemoveRequested(String key, int reason) {
435                 if (!mBubbleData.hasBubbleWithKey(key)) {
436                     return false;
437                 }
438                 NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry;
439 
440                 final boolean isClearAll = reason == REASON_CANCEL_ALL;
441                 final boolean isUserDimiss = reason == REASON_CANCEL;
442                 final boolean isAppCancel = reason == REASON_APP_CANCEL
443                         || reason == REASON_APP_CANCEL_ALL;
444 
445                 // Need to check for !appCancel here because the notification may have
446                 // previously been dismissed & entry.isRowDismissed would still be true
447                 boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel)
448                         || isClearAll || isUserDimiss;
449 
450                 // The bubble notification sticks around in the data as long as the bubble is
451                 // not dismissed and the app hasn't cancelled the notification.
452                 boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed()
453                         && userRemovedNotif;
454                 if (bubbleExtended) {
455                     entry.setShowInShadeWhenBubble(false);
456                     if (mStackView != null) {
457                         mStackView.updateDotVisibility(entry.key);
458                     }
459                     mNotificationEntryManager.updateNotifications();
460                     return true;
461                 } else if (!userRemovedNotif && !entry.isBubbleDismissed()) {
462                     // This wasn't a user removal so we should remove the bubble as well
463                     mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
464                     return false;
465                 }
466                 return false;
467             }
468         };
469 
470     @SuppressWarnings("FieldCanBeLocal")
471     private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
472         @Override
473         public void onPendingEntryAdded(NotificationEntry entry) {
474             if (!areBubblesEnabled(mContext)) {
475                 return;
476             }
477             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
478                     && canLaunchInActivityView(mContext, entry)) {
479                 updateShowInShadeForSuppressNotification(entry);
480             }
481         }
482 
483         @Override
484         public void onEntryInflated(NotificationEntry entry, @InflationFlag int inflatedFlags) {
485             if (!areBubblesEnabled(mContext)) {
486                 return;
487             }
488             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
489                     && canLaunchInActivityView(mContext, entry)) {
490                 updateBubble(entry);
491             }
492         }
493 
494         @Override
495         public void onPreEntryUpdated(NotificationEntry entry) {
496             if (!areBubblesEnabled(mContext)) {
497                 return;
498             }
499             boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
500                     && canLaunchInActivityView(mContext, entry);
501             if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) {
502                 // It was previously a bubble but no longer a bubble -- lets remove it
503                 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE);
504             } else if (shouldBubble) {
505                 updateShowInShadeForSuppressNotification(entry);
506                 entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed
507                 updateBubble(entry);
508             }
509         }
510 
511         @Override
512         public void onNotificationRankingUpdated(RankingMap rankingMap) {
513             // Forward to BubbleData to block any bubbles which should no longer be shown
514             mBubbleData.notificationRankingUpdated(rankingMap);
515         }
516     };
517 
518     @SuppressWarnings("FieldCanBeLocal")
519     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
520 
521         @Override
522         public void applyUpdate(BubbleData.Update update) {
523             if (mStackView == null && update.addedBubble != null) {
524                 // Lazy init stack view when the first bubble is added.
525                 ensureStackViewCreated();
526             }
527 
528             // If not yet initialized, ignore all other changes.
529             if (mStackView == null) {
530                 return;
531             }
532 
533             if (update.addedBubble != null) {
534                 mStackView.addBubble(update.addedBubble);
535             }
536 
537             // Collapsing? Do this first before remaining steps.
538             if (update.expandedChanged && !update.expanded) {
539                 mStackView.setExpanded(false);
540             }
541 
542             // Do removals, if any.
543             for (Pair<Bubble, Integer> removed : update.removedBubbles) {
544                 final Bubble bubble = removed.first;
545                 @DismissReason final int reason = removed.second;
546                 mStackView.removeBubble(bubble);
547 
548                 if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
549                         && !bubble.entry.showInShadeWhenBubble()) {
550                     // The bubble is gone & the notification is gone, time to actually remove it
551                     mNotificationEntryManager.performRemoveNotification(bubble.entry.notification,
552                             UNDEFINED_DISMISS_REASON);
553                 } else {
554                     // Update the flag for SysUI
555                     bubble.entry.notification.getNotification().flags &= ~FLAG_BUBBLE;
556 
557                     // Make sure NoMan knows it's not a bubble anymore so anyone querying it will
558                     // get right result back
559                     try {
560                         mBarService.onNotificationBubbleChanged(bubble.getKey(),
561                                 false /* isBubble */);
562                     } catch (RemoteException e) {
563                         // Bad things have happened
564                     }
565                 }
566             }
567 
568             if (update.updatedBubble != null) {
569                 mStackView.updateBubble(update.updatedBubble);
570             }
571 
572             if (update.orderChanged) {
573                 mStackView.updateBubbleOrder(update.bubbles);
574             }
575 
576             if (update.selectionChanged) {
577                 mStackView.setSelectedBubble(update.selectedBubble);
578             }
579 
580             // Expanding? Apply this last.
581             if (update.expandedChanged && update.expanded) {
582                 mStackView.setExpanded(true);
583             }
584 
585             mNotificationEntryManager.updateNotifications();
586             updateStack();
587 
588             if (DEBUG) {
589                 Log.d(TAG, "[BubbleData]");
590                 Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(),
591                         mBubbleData.getSelectedBubble()));
592 
593                 if (mStackView != null) {
594                     Log.d(TAG, "[BubbleStackView]");
595                     Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(),
596                             mStackView.getExpandedBubble()));
597                 }
598             }
599         }
600     };
601 
602     /**
603      * Updates the stack view's suppression flags from the latest config from the zen (do not
604      * disturb) controller.
605      */
updateStackViewForZenConfig()606     private void updateStackViewForZenConfig() {
607         final ZenModeConfig zenModeConfig = mZenModeController.getConfig();
608 
609         if (zenModeConfig == null || mStackView == null) {
610             return;
611         }
612 
613         final int suppressedEffects = zenModeConfig.suppressedVisualEffects;
614         final boolean hideNotificationDotsSelected =
615                 (suppressedEffects & SUPPRESSED_EFFECT_BADGE) != 0;
616         final boolean dontPopNotifsOnScreenSelected =
617                 (suppressedEffects & SUPPRESSED_EFFECT_PEEK) != 0;
618         final boolean hideFromPullDownShadeSelected =
619                 (suppressedEffects & SUPPRESSED_EFFECT_NOTIFICATION_LIST) != 0;
620 
621         final boolean dndEnabled = mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF;
622 
623         mStackView.setSuppressNewDot(
624                 dndEnabled && hideNotificationDotsSelected);
625         mStackView.setSuppressFlyout(
626                 dndEnabled && (dontPopNotifsOnScreenSelected
627                         || hideFromPullDownShadeSelected));
628     }
629 
630     /**
631      * Lets any listeners know if bubble state has changed.
632      * Updates the visibility of the bubbles based on current state.
633      * Does not un-bubble, just hides or un-hides. Notifies any
634      * {@link BubbleStateChangeListener}s of visibility changes.
635      * Updates stack description for TalkBack focus.
636      */
updateStack()637     public void updateStack() {
638         if (mStackView == null) {
639             return;
640         }
641         if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) {
642             // Bubbles only appear in unlocked shade
643             mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE);
644         } else if (mStackView != null) {
645             mStackView.setVisibility(INVISIBLE);
646         }
647 
648         // Let listeners know if bubble state changed.
649         boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
650         boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
651         mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
652         if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
653             mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
654         }
655 
656         mStackView.updateContentDescription();
657     }
658 
659     /**
660      * Rect indicating the touchable region for the bubble stack / expanded stack.
661      */
getTouchableRegion()662     public Rect getTouchableRegion() {
663         if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
664             return null;
665         }
666         mStackView.getBoundsOnScreen(mTempRect);
667         return mTempRect;
668     }
669 
670     /**
671      * The display id of the expanded view, if the stack is expanded and not occluded by the
672      * status bar, otherwise returns {@link Display#INVALID_DISPLAY}.
673      */
getExpandedDisplayId(Context context)674     public int getExpandedDisplayId(Context context) {
675         if (mStackView == null) {
676             return INVALID_DISPLAY;
677         }
678         boolean defaultDisplay = context.getDisplay() != null
679                 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY;
680         Bubble b = mStackView.getExpandedBubble();
681         if (defaultDisplay && b != null && isStackExpanded()
682                 && !mStatusBarWindowController.getPanelExpanded()) {
683             return b.expandedView.getVirtualDisplayId();
684         }
685         return INVALID_DISPLAY;
686     }
687 
688     @VisibleForTesting
getStackView()689     BubbleStackView getStackView() {
690         return mStackView;
691     }
692 
693     /**
694      * Whether the notification should automatically bubble or not. Gated by secure settings flags.
695      */
696     @VisibleForTesting
shouldAutoBubbleForFlags(Context context, NotificationEntry entry)697     protected boolean shouldAutoBubbleForFlags(Context context, NotificationEntry entry) {
698         if (entry.isBubbleDismissed()) {
699             return false;
700         }
701         StatusBarNotification n = entry.notification;
702 
703         boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
704         boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
705         boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
706 
707         boolean hasRemoteInput = false;
708         if (n.getNotification().actions != null) {
709             for (Notification.Action action : n.getNotification().actions) {
710                 if (action.getRemoteInputs() != null) {
711                     hasRemoteInput = true;
712                     break;
713                 }
714             }
715         }
716         boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
717                 && n.isOngoing();
718         boolean isMusic = n.getNotification().hasMediaSession();
719         boolean isImportantOngoing = isMusic || isCall;
720 
721         Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
722         boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
723         boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
724         return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
725                 || (isImportantOngoing && autoBubbleOngoing)
726                 || autoBubbleAll;
727     }
728 
updateShowInShadeForSuppressNotification(NotificationEntry entry)729     private void updateShowInShadeForSuppressNotification(NotificationEntry entry) {
730         boolean suppressNotification = entry.getBubbleMetadata() != null
731                 && entry.getBubbleMetadata().isNotificationSuppressed()
732                 && isForegroundApp(mContext, entry.notification.getPackageName());
733         entry.setShowInShadeWhenBubble(!suppressNotification);
734     }
735 
formatBubblesString(List<Bubble> bubbles, Bubble selected)736     static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
737         StringBuilder sb = new StringBuilder();
738         for (Bubble bubble : bubbles) {
739             if (bubble == null) {
740                 sb.append("   <null> !!!!!\n");
741             } else {
742                 boolean isSelected = (bubble == selected);
743                 sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n",
744                         ((isSelected) ? "->" : "  "),
745                         bubble.getLastActivity(),
746                         (bubble.isOngoing() ? 1 : 0),
747                         bubble.getKey()));
748             }
749         }
750         return sb.toString();
751     }
752 
753     /**
754      * Return true if the applications with the package name is running in foreground.
755      *
756      * @param context application context.
757      * @param pkgName application package name.
758      */
isForegroundApp(Context context, String pkgName)759     public static boolean isForegroundApp(Context context, String pkgName) {
760         ActivityManager am = context.getSystemService(ActivityManager.class);
761         List<RunningTaskInfo> tasks = am.getRunningTasks(1 /* maxNum */);
762         return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
763     }
764 
765     /**
766      * This task stack listener is responsible for responding to tasks moved to the front
767      * which are on the default (main) display. When this happens, expanded bubbles must be
768      * collapsed so the user may interact with the app which was just moved to the front.
769      * <p>
770      * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches
771      * these calls via a main thread Handler.
772      */
773     @MainThread
774     private class BubbleTaskStackListener extends TaskStackChangeListener {
775 
776         @Override
onTaskMovedToFront(RunningTaskInfo taskInfo)777         public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
778             if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) {
779                 mBubbleData.setExpanded(false);
780             }
781         }
782 
783         @Override
onActivityLaunchOnSecondaryDisplayRerouted()784         public void onActivityLaunchOnSecondaryDisplayRerouted() {
785             if (mStackView != null) {
786                 mBubbleData.setExpanded(false);
787             }
788         }
789 
790         @Override
onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)791         public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
792             if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) {
793                 mBubbleData.setExpanded(false);
794             }
795         }
796     }
797 
shouldAutoBubbleMessages(Context context)798     private static boolean shouldAutoBubbleMessages(Context context) {
799         return Settings.Secure.getInt(context.getContentResolver(),
800                 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
801     }
802 
shouldAutoBubbleOngoing(Context context)803     private static boolean shouldAutoBubbleOngoing(Context context) {
804         return Settings.Secure.getInt(context.getContentResolver(),
805                 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
806     }
807 
shouldAutoBubbleAll(Context context)808     private static boolean shouldAutoBubbleAll(Context context) {
809         return Settings.Secure.getInt(context.getContentResolver(),
810                 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
811     }
812 
shouldUseContentIntent(Context context)813     static boolean shouldUseContentIntent(Context context) {
814         return Settings.Secure.getInt(context.getContentResolver(),
815                 ENABLE_BUBBLE_CONTENT_INTENT, 0) != 0;
816     }
817 
areBubblesEnabled(Context context)818     private static boolean areBubblesEnabled(Context context) {
819         return Settings.Secure.getInt(context.getContentResolver(),
820                 ENABLE_BUBBLES, 1) != 0;
821     }
822 
823     /** Default stiffness to use for bubble physics animations. */
getBubbleStiffness(Context context, int defaultStiffness)824     public static int getBubbleStiffness(Context context, int defaultStiffness) {
825         return Settings.Secure.getInt(
826                 context.getContentResolver(), BUBBLE_STIFFNESS, defaultStiffness);
827     }
828 
829     /** Default bounciness/damping ratio to use for bubble physics animations. */
getBubbleBounciness(Context context, float defaultBounciness)830     public static float getBubbleBounciness(Context context, float defaultBounciness) {
831         return Settings.Secure.getInt(
832                 context.getContentResolver(),
833                 BUBBLE_BOUNCINESS,
834                 (int) (defaultBounciness * 100)) / 100f;
835     }
836 
837     /**
838      * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
839      *
840      * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically
841      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
842      *
843      * @param context the context to use.
844      * @param entry the entry to bubble.
845      */
canLaunchInActivityView(Context context, NotificationEntry entry)846     static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
847         PendingIntent intent = entry.getBubbleMetadata() != null
848                 ? entry.getBubbleMetadata().getIntent()
849                 : null;
850         if (intent == null) {
851             Log.w(TAG, "Unable to create bubble -- no intent");
852             return false;
853         }
854         ActivityInfo info =
855                 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
856         if (info == null) {
857             Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
858                     + intent);
859             return false;
860         }
861         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
862             Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
863                     + intent);
864             return false;
865         }
866         if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
867             Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always "
868                     + "for intent: " + intent);
869             return false;
870         }
871         if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) {
872             Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: "
873                     + intent);
874             return false;
875         }
876         return true;
877     }
878 
879     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
880     private class BubblesImeListener extends IPinnedStackListener.Stub {
881 
882         @Override
onListenerRegistered(IPinnedStackController controller)883         public void onListenerRegistered(IPinnedStackController controller) throws RemoteException {
884         }
885 
886         @Override
onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)887         public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
888                 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
889                 int displayRotation) throws RemoteException {}
890 
891         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)892         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
893             if (mStackView != null && mStackView.getBubbleCount() > 0) {
894                 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight));
895             }
896         }
897 
898         @Override
onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)899         public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)
900                 throws RemoteException {}
901 
902         @Override
onMinimizedStateChanged(boolean isMinimized)903         public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {}
904 
905         @Override
onActionsChanged(ParceledListSlice actions)906         public void onActionsChanged(ParceledListSlice actions) throws RemoteException {}
907     }
908 }
909