• 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.statusbar.phone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.graphics.Region;
26 import android.util.Log;
27 import android.util.Pools;
28 import android.view.DisplayCutout;
29 import android.view.Gravity;
30 import android.view.View;
31 import android.view.ViewTreeObserver;
32 
33 import androidx.collection.ArraySet;
34 
35 import com.android.systemui.Dependency;
36 import com.android.systemui.Dumpable;
37 import com.android.systemui.R;
38 import com.android.systemui.ScreenDecorations;
39 import com.android.systemui.plugins.statusbar.StatusBarStateController;
40 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
41 import com.android.systemui.statusbar.StatusBarState;
42 import com.android.systemui.statusbar.notification.VisualStabilityManager;
43 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
45 import com.android.systemui.statusbar.policy.ConfigurationController;
46 import com.android.systemui.statusbar.policy.HeadsUpManager;
47 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.util.HashSet;
52 import java.util.Stack;
53 
54 /**
55  * A implementation of HeadsUpManager for phone and car.
56  */
57 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
58         VisualStabilityManager.Callback, OnHeadsUpChangedListener,
59         ConfigurationController.ConfigurationListener, StateListener {
60     private static final String TAG = "HeadsUpManagerPhone";
61 
62     private final View mStatusBarWindowView;
63     private final NotificationGroupManager mGroupManager;
64     private final VisualStabilityManager mVisualStabilityManager;
65     private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
66     private boolean mReleaseOnExpandFinish;
67 
68     private int mStatusBarHeight;
69     private int mHeadsUpInset;
70     private int mDisplayCutoutTouchableRegionSize;
71     private boolean mTrackingHeadsUp;
72     private HashSet<String> mSwipedOutKeys = new HashSet<>();
73     private HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
74     private ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
75             = new ArraySet<>();
76     private boolean mIsExpanded;
77     private int[] mTmpTwoArray = new int[2];
78     private boolean mHeadsUpGoingAway;
79     private int mStatusBarState;
80     private Region mTouchableRegion = new Region();
81 
82     private AnimationStateHandler mAnimationStateHandler;
83 
84     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
85         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
86 
87         @Override
88         public HeadsUpEntryPhone acquire() {
89             if (!mPoolObjects.isEmpty()) {
90                 return mPoolObjects.pop();
91             }
92             return new HeadsUpEntryPhone();
93         }
94 
95         @Override
96         public boolean release(@NonNull HeadsUpEntryPhone instance) {
97             mPoolObjects.push(instance);
98             return true;
99         }
100     };
101 
102     ///////////////////////////////////////////////////////////////////////////////////////////////
103     //  Constructor:
104 
HeadsUpManagerPhone(@onNull final Context context, @NonNull View statusBarWindowView, @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, @NonNull VisualStabilityManager visualStabilityManager)105     public HeadsUpManagerPhone(@NonNull final Context context,
106                                @NonNull View statusBarWindowView,
107                                @NonNull NotificationGroupManager groupManager,
108                                @NonNull StatusBar bar,
109                                @NonNull VisualStabilityManager visualStabilityManager) {
110         super(context);
111 
112         mStatusBarWindowView = statusBarWindowView;
113         mStatusBarTouchableRegionManager = new StatusBarTouchableRegionManager(context, this, bar,
114                 statusBarWindowView);
115         mGroupManager = groupManager;
116         mVisualStabilityManager = visualStabilityManager;
117 
118         initResources();
119 
120         addListener(new OnHeadsUpChangedListener() {
121             @Override
122             public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
123                 if (Log.isLoggable(TAG, Log.WARN)) {
124                     Log.w(TAG, "onHeadsUpPinnedModeChanged");
125                 }
126                 mStatusBarTouchableRegionManager.updateTouchableRegion();
127             }
128         });
129         Dependency.get(StatusBarStateController.class).addCallback(this);
130     }
131 
setAnimationStateHandler(AnimationStateHandler handler)132     public void setAnimationStateHandler(AnimationStateHandler handler) {
133         mAnimationStateHandler = handler;
134     }
135 
initResources()136     private void initResources() {
137         Resources resources = mContext.getResources();
138         mStatusBarHeight = resources.getDimensionPixelSize(
139                 com.android.internal.R.dimen.status_bar_height);
140         mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize(
141                 R.dimen.heads_up_status_bar_padding);
142         mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize(
143                 com.android.internal.R.dimen.display_cutout_touchable_region_size);
144     }
145 
146     @Override
onDensityOrFontScaleChanged()147     public void onDensityOrFontScaleChanged() {
148         super.onDensityOrFontScaleChanged();
149         initResources();
150     }
151 
152     @Override
onOverlayChanged()153     public void onOverlayChanged() {
154         initResources();
155     }
156 
157     ///////////////////////////////////////////////////////////////////////////////////////////////
158     //  Public methods:
159 
160     /**
161      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
162      * that a user might have consciously clicked on it.
163      *
164      * @param key the key of the touched notification
165      * @return whether the touch is invalid and should be discarded
166      */
shouldSwallowClick(@onNull String key)167     public boolean shouldSwallowClick(@NonNull String key) {
168         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
169         return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
170     }
171 
onExpandingFinished()172     public void onExpandingFinished() {
173         if (mReleaseOnExpandFinish) {
174             releaseAllImmediately();
175             mReleaseOnExpandFinish = false;
176         } else {
177             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
178                 if (isAlerting(entry.key)) {
179                     // Maybe the heads-up was removed already
180                     removeAlertEntry(entry.key);
181                 }
182             }
183         }
184         mEntriesToRemoveAfterExpand.clear();
185     }
186 
187     /**
188      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
189      * from the list even after a Heads Up Notification is gone.
190      */
setTrackingHeadsUp(boolean trackingHeadsUp)191     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
192         mTrackingHeadsUp = trackingHeadsUp;
193     }
194 
195     /**
196      * Notify that the status bar panel gets expanded or collapsed.
197      *
198      * @param isExpanded True to notify expanded, false to notify collapsed.
199      */
setIsPanelExpanded(boolean isExpanded)200     public void setIsPanelExpanded(boolean isExpanded) {
201         if (isExpanded != mIsExpanded) {
202             mIsExpanded = isExpanded;
203             if (isExpanded) {
204                 mHeadsUpGoingAway = false;
205             }
206             mStatusBarTouchableRegionManager.setIsStatusBarExpanded(isExpanded);
207             mStatusBarTouchableRegionManager.updateTouchableRegion();
208         }
209     }
210 
211     @Override
onStateChanged(int newState)212     public void onStateChanged(int newState) {
213         mStatusBarState = newState;
214     }
215 
216     /**
217      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
218      * animating out. This is used to keep the touchable regions in a sane state.
219      */
setHeadsUpGoingAway(boolean headsUpGoingAway)220     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
221         if (headsUpGoingAway != mHeadsUpGoingAway) {
222             mHeadsUpGoingAway = headsUpGoingAway;
223             if (!headsUpGoingAway) {
224                 mStatusBarTouchableRegionManager.updateTouchableRegionAfterLayout();
225             } else {
226                 mStatusBarTouchableRegionManager.updateTouchableRegion();
227             }
228         }
229     }
230 
isHeadsUpGoingAway()231     public boolean isHeadsUpGoingAway() {
232         return mHeadsUpGoingAway;
233     }
234 
235     /**
236      * Notifies that a remote input textbox in notification gets active or inactive.
237      *
238      * @param entry             The entry of the target notification.
239      * @param remoteInputActive True to notify active, False to notify inactive.
240      */
setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)241     public void setRemoteInputActive(
242             @NonNull NotificationEntry entry, boolean remoteInputActive) {
243         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
244         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
245             headsUpEntry.remoteInputActive = remoteInputActive;
246             if (remoteInputActive) {
247                 headsUpEntry.removeAutoRemovalCallbacks();
248             } else {
249                 headsUpEntry.updateEntry(false /* updatePostTime */);
250             }
251         }
252     }
253 
254     /**
255      * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up
256      * area if it's pinned until it's hidden again.
257      */
setMenuShown(@onNull NotificationEntry entry, boolean menuShown)258     public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) {
259         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.key);
260         if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) {
261             ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown);
262         }
263     }
264 
265     ///////////////////////////////////////////////////////////////////////////////////////////////
266     //  HeadsUpManager public methods overrides:
267 
268     @Override
isTrackingHeadsUp()269     public boolean isTrackingHeadsUp() {
270         return mTrackingHeadsUp;
271     }
272 
273     @Override
snooze()274     public void snooze() {
275         super.snooze();
276         mReleaseOnExpandFinish = true;
277     }
278 
addSwipedOutNotification(@onNull String key)279     public void addSwipedOutNotification(@NonNull String key) {
280         mSwipedOutKeys.add(key);
281     }
282 
283     ///////////////////////////////////////////////////////////////////////////////////////////////
284     //  Dumpable overrides:
285 
286     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)287     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
288         pw.println("HeadsUpManagerPhone state:");
289         dumpInternal(fd, pw, args);
290     }
291 
292     /**
293      * Update touch insets to include any area needed for touching a heads up notification.
294      *
295      * @param info Insets that will include heads up notification touch area after execution.
296      */
297     @Nullable
updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info)298     public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) {
299         info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
300         info.touchableRegion.set(calculateTouchableRegion());
301     }
302 
calculateTouchableRegion()303     public Region calculateTouchableRegion() {
304         NotificationEntry topEntry = getTopEntry();
305         // This call could be made in an inconsistent state while the pinnedMode hasn't been
306         // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's
307         // therefore also check if the topEntry is null.
308         if (!hasPinnedHeadsUp() || topEntry == null) {
309             mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
310             updateRegionForNotch(mTouchableRegion);
311 
312         } else {
313             if (topEntry.isChildInGroup()) {
314                 final NotificationEntry groupSummary =
315                         mGroupManager.getGroupSummary(topEntry.notification);
316                 if (groupSummary != null) {
317                     topEntry = groupSummary;
318                 }
319             }
320             ExpandableNotificationRow topRow = topEntry.getRow();
321             topRow.getLocationOnScreen(mTmpTwoArray);
322             int minX = mTmpTwoArray[0];
323             int maxX = mTmpTwoArray[0] + topRow.getWidth();
324             int height = topRow.getIntrinsicHeight();
325             mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
326         }
327         return mTouchableRegion;
328     }
329 
updateRegionForNotch(Region region)330     private void updateRegionForNotch(Region region) {
331         DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout();
332         if (cutout == null) {
333             return;
334         }
335 
336         // Expand touchable region such that we also catch touches that just start below the notch
337         // area.
338         Rect bounds = new Rect();
339         ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds);
340         bounds.offset(0, mDisplayCutoutTouchableRegionSize);
341         region.union(bounds);
342     }
343 
344     @Override
shouldExtendLifetime(NotificationEntry entry)345     public boolean shouldExtendLifetime(NotificationEntry entry) {
346         // We should not defer the removal if reordering isn't allowed since otherwise
347         // these won't disappear until reordering is allowed again, which happens only once
348         // the notification panel is collapsed again.
349         return mVisualStabilityManager.isReorderingAllowed() && super.shouldExtendLifetime(entry);
350     }
351 
352     @Override
onConfigChanged(Configuration newConfig)353     public void onConfigChanged(Configuration newConfig) {
354         initResources();
355     }
356 
357     ///////////////////////////////////////////////////////////////////////////////////////////////
358     //  VisualStabilityManager.Callback overrides:
359 
360     @Override
onReorderingAllowed()361     public void onReorderingAllowed() {
362         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
363         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
364             if (isAlerting(entry.key)) {
365                 // Maybe the heads-up was removed already
366                 removeAlertEntry(entry.key);
367             }
368         }
369         mEntriesToRemoveWhenReorderingAllowed.clear();
370         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
371     }
372 
373     ///////////////////////////////////////////////////////////////////////////////////////////////
374     //  HeadsUpManager utility (protected) methods overrides:
375 
376     @Override
createAlertEntry()377     protected HeadsUpEntry createAlertEntry() {
378         return mEntryPool.acquire();
379     }
380 
381     @Override
onAlertEntryRemoved(AlertEntry alertEntry)382     protected void onAlertEntryRemoved(AlertEntry alertEntry) {
383         super.onAlertEntryRemoved(alertEntry);
384         mEntryPool.release((HeadsUpEntryPhone) alertEntry);
385     }
386 
387     @Override
shouldHeadsUpBecomePinned(NotificationEntry entry)388     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
389         return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
390                 || super.shouldHeadsUpBecomePinned(entry);
391     }
392 
393     @Override
dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)394     protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
395         super.dumpInternal(fd, pw, args);
396         pw.print("  mBarState=");
397         pw.println(mStatusBarState);
398         pw.print("  mTouchableRegion=");
399         pw.println(mTouchableRegion);
400     }
401 
402     ///////////////////////////////////////////////////////////////////////////////////////////////
403     //  Private utility methods:
404 
405     @Nullable
getHeadsUpEntryPhone(@onNull String key)406     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
407         return (HeadsUpEntryPhone) mAlertEntries.get(key);
408     }
409 
410     @Nullable
getTopHeadsUpEntryPhone()411     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
412         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
413     }
414 
415     @Override
canRemoveImmediately(@onNull String key)416     protected boolean canRemoveImmediately(@NonNull String key) {
417         if (mSwipedOutKeys.contains(key)) {
418             // We always instantly dismiss views being manually swiped out.
419             mSwipedOutKeys.remove(key);
420             return true;
421         }
422 
423         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
424         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
425 
426         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
427     }
428 
429     ///////////////////////////////////////////////////////////////////////////////////////////////
430     //  HeadsUpEntryPhone:
431 
432     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
433 
434         private boolean mMenuShownPinned;
435 
436         @Override
isSticky()437         protected boolean isSticky() {
438             return super.isSticky() || mMenuShownPinned;
439         }
440 
setEntry(@onNull final NotificationEntry entry)441         public void setEntry(@NonNull final NotificationEntry entry) {
442             Runnable removeHeadsUpRunnable = () -> {
443                 if (!mVisualStabilityManager.isReorderingAllowed()) {
444                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
445                     mVisualStabilityManager.addReorderingAllowedCallback(
446                             HeadsUpManagerPhone.this);
447                 } else if (!mTrackingHeadsUp) {
448                     removeAlertEntry(entry.key);
449                 } else {
450                     mEntriesToRemoveAfterExpand.add(entry);
451                 }
452             };
453 
454             setEntry(entry, removeHeadsUpRunnable);
455         }
456 
457         @Override
updateEntry(boolean updatePostTime)458         public void updateEntry(boolean updatePostTime) {
459             super.updateEntry(updatePostTime);
460 
461             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
462                 mEntriesToRemoveAfterExpand.remove(mEntry);
463             }
464             if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
465                 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
466             }
467         }
468 
469         @Override
setExpanded(boolean expanded)470         public void setExpanded(boolean expanded) {
471             if (this.expanded == expanded) {
472                 return;
473             }
474 
475             this.expanded = expanded;
476             if (expanded) {
477                 removeAutoRemovalCallbacks();
478             } else {
479                 updateEntry(false /* updatePostTime */);
480             }
481         }
482 
setMenuShownPinned(boolean menuShownPinned)483         public void setMenuShownPinned(boolean menuShownPinned) {
484             if (mMenuShownPinned == menuShownPinned) {
485                 return;
486             }
487 
488             mMenuShownPinned = menuShownPinned;
489             if (menuShownPinned) {
490                 removeAutoRemovalCallbacks();
491             } else {
492                 updateEntry(false /* updatePostTime */);
493             }
494         }
495 
496         @Override
reset()497         public void reset() {
498             super.reset();
499             mMenuShownPinned = false;
500         }
501     }
502 
503     public interface AnimationStateHandler {
setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)504         void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed);
505     }
506 }
507