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.support.v4.util.ArraySet;
25 import android.util.Log;
26 import android.util.Pools;
27 import android.view.View;
28 import android.view.ViewTreeObserver;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.systemui.Dumpable;
32 import com.android.systemui.R;
33 import com.android.systemui.statusbar.ExpandableNotificationRow;
34 import com.android.systemui.statusbar.NotificationData;
35 import com.android.systemui.statusbar.StatusBarState;
36 import com.android.systemui.statusbar.notification.VisualStabilityManager;
37 import com.android.systemui.statusbar.policy.ConfigurationController;
38 import com.android.systemui.statusbar.policy.HeadsUpManager;
39 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.HashSet;
44 import java.util.Stack;
45 
46 /**
47  * A implementation of HeadsUpManager for phone and car.
48  */
49 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
50        ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
51        OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener {
52     private static final String TAG = "HeadsUpManagerPhone";
53     private static final boolean DEBUG = false;
54 
55     private final View mStatusBarWindowView;
56     private final NotificationGroupManager mGroupManager;
57     private final StatusBar mBar;
58     private final VisualStabilityManager mVisualStabilityManager;
59     private boolean mReleaseOnExpandFinish;
60 
61     private int mStatusBarHeight;
62     private int mHeadsUpInset;
63     private boolean mTrackingHeadsUp;
64     private HashSet<String> mSwipedOutKeys = new HashSet<>();
65     private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
66     private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
67             = new ArraySet<>();
68     private boolean mIsExpanded;
69     private int[] mTmpTwoArray = new int[2];
70     private boolean mHeadsUpGoingAway;
71     private boolean mWaitingOnCollapseWhenGoingAway;
72     private boolean mIsObserving;
73     private int mStatusBarState;
74 
75     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
76         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
77 
78         @Override
79         public HeadsUpEntryPhone acquire() {
80             if (!mPoolObjects.isEmpty()) {
81                 return mPoolObjects.pop();
82             }
83             return new HeadsUpEntryPhone();
84         }
85 
86         @Override
87         public boolean release(@NonNull HeadsUpEntryPhone instance) {
88             mPoolObjects.push(instance);
89             return true;
90         }
91     };
92 
93     ///////////////////////////////////////////////////////////////////////////////////////////////
94     //  Constructor:
95 
HeadsUpManagerPhone(@onNull final Context context, @NonNull View statusBarWindowView, @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, @NonNull VisualStabilityManager visualStabilityManager)96     public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView,
97             @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar,
98             @NonNull VisualStabilityManager visualStabilityManager) {
99         super(context);
100 
101         mStatusBarWindowView = statusBarWindowView;
102         mGroupManager = groupManager;
103         mBar = bar;
104         mVisualStabilityManager = visualStabilityManager;
105 
106         initResources();
107 
108         addListener(new OnHeadsUpChangedListener() {
109             @Override
110             public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
111                 if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
112                 updateTouchableRegionListener();
113             }
114         });
115     }
116 
initResources()117     private void initResources() {
118         Resources resources = mContext.getResources();
119         mStatusBarHeight = resources.getDimensionPixelSize(
120                 com.android.internal.R.dimen.status_bar_height);
121         mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize(
122                 R.dimen.heads_up_status_bar_padding);
123     }
124 
125     @Override
onDensityOrFontScaleChanged()126     public void onDensityOrFontScaleChanged() {
127         super.onDensityOrFontScaleChanged();
128         initResources();
129     }
130 
131     ///////////////////////////////////////////////////////////////////////////////////////////////
132     //  Public methods:
133 
134     /**
135      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
136      * that a user might have consciously clicked on it.
137      *
138      * @param key the key of the touched notification
139      * @return whether the touch is invalid and should be discarded
140      */
shouldSwallowClick(@onNull String key)141     public boolean shouldSwallowClick(@NonNull String key) {
142         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
143         return entry != null && mClock.currentTimeMillis() < entry.postTime;
144     }
145 
onExpandingFinished()146     public void onExpandingFinished() {
147         if (mReleaseOnExpandFinish) {
148             releaseAllImmediately();
149             mReleaseOnExpandFinish = false;
150         } else {
151             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
152                 if (isHeadsUp(entry.key)) {
153                     // Maybe the heads-up was removed already
154                     removeHeadsUpEntry(entry);
155                 }
156             }
157         }
158         mEntriesToRemoveAfterExpand.clear();
159     }
160 
161     /**
162      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
163      * from the list even after a Heads Up Notification is gone.
164      */
setTrackingHeadsUp(boolean trackingHeadsUp)165     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
166         mTrackingHeadsUp = trackingHeadsUp;
167     }
168 
169     /**
170      * Notify that the status bar panel gets expanded or collapsed.
171      *
172      * @param isExpanded True to notify expanded, false to notify collapsed.
173      */
setIsPanelExpanded(boolean isExpanded)174     public void setIsPanelExpanded(boolean isExpanded) {
175         if (isExpanded != mIsExpanded) {
176             mIsExpanded = isExpanded;
177             if (isExpanded) {
178                 // make sure our state is sane
179                 mWaitingOnCollapseWhenGoingAway = false;
180                 mHeadsUpGoingAway = false;
181                 updateTouchableRegionListener();
182             }
183         }
184     }
185 
186     /**
187      * Set the current state of the statusbar.
188      */
setStatusBarState(int statusBarState)189     public void setStatusBarState(int statusBarState) {
190         mStatusBarState = statusBarState;
191     }
192 
193     /**
194      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
195      * animating out. This is used to keep the touchable regions in a sane state.
196      */
setHeadsUpGoingAway(boolean headsUpGoingAway)197     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
198         if (headsUpGoingAway != mHeadsUpGoingAway) {
199             mHeadsUpGoingAway = headsUpGoingAway;
200             if (!headsUpGoingAway) {
201                 waitForStatusBarLayout();
202             }
203             updateTouchableRegionListener();
204         }
205     }
206 
207     /**
208      * Notifies that a remote input textbox in notification gets active or inactive.
209      * @param entry The entry of the target notification.
210      * @param remoteInputActive True to notify active, False to notify inactive.
211      */
setRemoteInputActive( @onNull NotificationData.Entry entry, boolean remoteInputActive)212     public void setRemoteInputActive(
213             @NonNull NotificationData.Entry entry, boolean remoteInputActive) {
214         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
215         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
216             headsUpEntry.remoteInputActive = remoteInputActive;
217             if (remoteInputActive) {
218                 headsUpEntry.removeAutoRemovalCallbacks();
219             } else {
220                 headsUpEntry.updateEntry(false /* updatePostTime */);
221             }
222         }
223     }
224 
225     @VisibleForTesting
removeMinimumDisplayTimeForTesting()226     public void removeMinimumDisplayTimeForTesting() {
227         mMinimumDisplayTime = 0;
228         mHeadsUpNotificationDecay = 0;
229         mTouchAcceptanceDelay = 0;
230     }
231 
232     ///////////////////////////////////////////////////////////////////////////////////////////////
233     //  HeadsUpManager public methods overrides:
234 
235     @Override
isTrackingHeadsUp()236     public boolean isTrackingHeadsUp() {
237         return mTrackingHeadsUp;
238     }
239 
240     @Override
snooze()241     public void snooze() {
242         super.snooze();
243         mReleaseOnExpandFinish = true;
244     }
245 
246     /**
247      * React to the removal of the notification in the heads up.
248      *
249      * @return true if the notification was removed and false if it still needs to be kept around
250      * for a bit since it wasn't shown long enough
251      */
252     @Override
removeNotification(@onNull String key, boolean ignoreEarliestRemovalTime)253     public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
254         if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
255             return super.removeNotification(key, ignoreEarliestRemovalTime);
256         } else {
257             HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
258             entry.removeAsSoonAsPossible();
259             return false;
260         }
261     }
262 
addSwipedOutNotification(@onNull String key)263     public void addSwipedOutNotification(@NonNull String key) {
264         mSwipedOutKeys.add(key);
265     }
266 
267     ///////////////////////////////////////////////////////////////////////////////////////////////
268     //  Dumpable overrides:
269 
270     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)271     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
272         pw.println("HeadsUpManagerPhone state:");
273         dumpInternal(fd, pw, args);
274     }
275 
276     ///////////////////////////////////////////////////////////////////////////////////////////////
277     //  ViewTreeObserver.OnComputeInternalInsetsListener overrides:
278 
279     /**
280      * Overridden from TreeObserver.
281      */
282     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)283     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
284         if (mIsExpanded || mBar.isBouncerShowing()) {
285             // The touchable region is always the full area when expanded
286             return;
287         }
288         if (hasPinnedHeadsUp()) {
289             ExpandableNotificationRow topEntry = getTopEntry().row;
290             if (topEntry.isChildInGroup()) {
291                 final ExpandableNotificationRow groupSummary
292                         = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
293                 if (groupSummary != null) {
294                     topEntry = groupSummary;
295                 }
296             }
297             topEntry.getLocationOnScreen(mTmpTwoArray);
298             int minX = mTmpTwoArray[0];
299             int maxX = mTmpTwoArray[0] + topEntry.getWidth();
300             int height = topEntry.getIntrinsicHeight();
301 
302             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
303             info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
304         } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
305             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
306             info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
307         }
308     }
309 
310     @Override
onConfigChanged(Configuration newConfig)311     public void onConfigChanged(Configuration newConfig) {
312         Resources resources = mContext.getResources();
313         mStatusBarHeight = resources.getDimensionPixelSize(
314                 com.android.internal.R.dimen.status_bar_height);
315     }
316 
317     ///////////////////////////////////////////////////////////////////////////////////////////////
318     //  VisualStabilityManager.Callback overrides:
319 
320     @Override
onReorderingAllowed()321     public void onReorderingAllowed() {
322         mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
323         for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
324             if (isHeadsUp(entry.key)) {
325                 // Maybe the heads-up was removed already
326                 removeHeadsUpEntry(entry);
327             }
328         }
329         mEntriesToRemoveWhenReorderingAllowed.clear();
330         mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
331     }
332 
333     ///////////////////////////////////////////////////////////////////////////////////////////////
334     //  HeadsUpManager utility (protected) methods overrides:
335 
336     @Override
createHeadsUpEntry()337     protected HeadsUpEntry createHeadsUpEntry() {
338         return mEntryPool.acquire();
339     }
340 
341     @Override
releaseHeadsUpEntry(HeadsUpEntry entry)342     protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
343         entry.reset();
344         mEntryPool.release((HeadsUpEntryPhone) entry);
345     }
346 
347     @Override
shouldHeadsUpBecomePinned(NotificationData.Entry entry)348     protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
349           return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
350                   || super.shouldHeadsUpBecomePinned(entry);
351     }
352 
353     @Override
dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)354     protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
355         super.dumpInternal(fd, pw, args);
356         pw.print("  mStatusBarState="); pw.println(mStatusBarState);
357     }
358 
359     ///////////////////////////////////////////////////////////////////////////////////////////////
360     //  Private utility methods:
361 
362     @Nullable
getHeadsUpEntryPhone(@onNull String key)363     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
364         return (HeadsUpEntryPhone) getHeadsUpEntry(key);
365     }
366 
367     @Nullable
getTopHeadsUpEntryPhone()368     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
369         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
370     }
371 
wasShownLongEnough(@onNull String key)372     private boolean wasShownLongEnough(@NonNull String key) {
373         if (mSwipedOutKeys.contains(key)) {
374             // We always instantly dismiss views being manually swiped out.
375             mSwipedOutKeys.remove(key);
376             return true;
377         }
378 
379         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
380         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
381         return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough();
382     }
383 
384     /**
385      * We need to wait on the whole panel to collapse, before we can remove the touchable region
386      * listener.
387      */
waitForStatusBarLayout()388     private void waitForStatusBarLayout() {
389         mWaitingOnCollapseWhenGoingAway = true;
390         mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
391             @Override
392             public void onLayoutChange(View v, int left, int top, int right, int bottom,
393                     int oldLeft,
394                     int oldTop, int oldRight, int oldBottom) {
395                 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
396                     mStatusBarWindowView.removeOnLayoutChangeListener(this);
397                     mWaitingOnCollapseWhenGoingAway = false;
398                     updateTouchableRegionListener();
399                 }
400             }
401         });
402     }
403 
updateTouchableRegionListener()404     private void updateTouchableRegionListener() {
405         boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway
406                 || mWaitingOnCollapseWhenGoingAway;
407         if (shouldObserve == mIsObserving) {
408             return;
409         }
410         if (shouldObserve) {
411             mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
412             mStatusBarWindowView.requestLayout();
413         } else {
414             mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
415         }
416         mIsObserving = shouldObserve;
417     }
418 
419     ///////////////////////////////////////////////////////////////////////////////////////////////
420     //  HeadsUpEntryPhone:
421 
422     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
setEntry(@onNull final NotificationData.Entry entry)423         public void setEntry(@NonNull final NotificationData.Entry entry) {
424            Runnable removeHeadsUpRunnable = () -> {
425                 if (!mVisualStabilityManager.isReorderingAllowed()) {
426                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
427                     mVisualStabilityManager.addReorderingAllowedCallback(
428                             HeadsUpManagerPhone.this);
429                 } else if (!mTrackingHeadsUp) {
430                     removeHeadsUpEntry(entry);
431                 } else {
432                     mEntriesToRemoveAfterExpand.add(entry);
433                 }
434             };
435 
436             super.setEntry(entry, removeHeadsUpRunnable);
437         }
438 
wasShownLongEnough()439         public boolean wasShownLongEnough() {
440             return earliestRemovaltime < mClock.currentTimeMillis();
441         }
442 
443         @Override
updateEntry(boolean updatePostTime)444         public void updateEntry(boolean updatePostTime) {
445             super.updateEntry(updatePostTime);
446 
447             if (mEntriesToRemoveAfterExpand.contains(entry)) {
448                 mEntriesToRemoveAfterExpand.remove(entry);
449             }
450             if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
451                 mEntriesToRemoveWhenReorderingAllowed.remove(entry);
452             }
453         }
454 
455         @Override
expanded(boolean expanded)456         public void expanded(boolean expanded) {
457             if (this.expanded == expanded) {
458                 return;
459             }
460 
461             this.expanded = expanded;
462             if (expanded) {
463                 removeAutoRemovalCallbacks();
464             } else {
465                 updateEntry(false /* updatePostTime */);
466             }
467         }
468     }
469 }
470