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
17package com.android.systemui.statusbar.phone;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.support.v4.util.ArraySet;
25import android.util.Log;
26import android.util.Pools;
27import android.view.View;
28import android.view.ViewTreeObserver;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.systemui.Dumpable;
32import com.android.systemui.R;
33import com.android.systemui.statusbar.ExpandableNotificationRow;
34import com.android.systemui.statusbar.NotificationData;
35import com.android.systemui.statusbar.StatusBarState;
36import com.android.systemui.statusbar.notification.VisualStabilityManager;
37import com.android.systemui.statusbar.policy.ConfigurationController;
38import com.android.systemui.statusbar.policy.HeadsUpManager;
39import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
40
41import java.io.FileDescriptor;
42import java.io.PrintWriter;
43import java.util.HashSet;
44import java.util.Stack;
45
46/**
47 * A implementation of HeadsUpManager for phone and car.
48 */
49public 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
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
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
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     */
141    public boolean shouldSwallowClick(@NonNull String key) {
142        HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
143        return entry != null && mClock.currentTimeMillis() < entry.postTime;
144    }
145
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     */
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     */
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     */
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     */
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     */
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
226    public void removeMinimumDisplayTimeForTesting() {
227        mMinimumDisplayTime = 0;
228        mHeadsUpNotificationDecay = 0;
229        mTouchAcceptanceDelay = 0;
230    }
231
232    ///////////////////////////////////////////////////////////////////////////////////////////////
233    //  HeadsUpManager public methods overrides:
234
235    @Override
236    public boolean isTrackingHeadsUp() {
237        return mTrackingHeadsUp;
238    }
239
240    @Override
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
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
263    public void addSwipedOutNotification(@NonNull String key) {
264        mSwipedOutKeys.add(key);
265    }
266
267    ///////////////////////////////////////////////////////////////////////////////////////////////
268    //  Dumpable overrides:
269
270    @Override
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
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
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
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
337    protected HeadsUpEntry createHeadsUpEntry() {
338        return mEntryPool.acquire();
339    }
340
341    @Override
342    protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
343        entry.reset();
344        mEntryPool.release((HeadsUpEntryPhone) entry);
345    }
346
347    @Override
348    protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
349          return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
350                  || super.shouldHeadsUpBecomePinned(entry);
351    }
352
353    @Override
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
363    private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
364        return (HeadsUpEntryPhone) getHeadsUpEntry(key);
365    }
366
367    @Nullable
368    private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
369        return (HeadsUpEntryPhone) getTopHeadsUpEntry();
370    }
371
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     */
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
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 {
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
439        public boolean wasShownLongEnough() {
440            return earliestRemovaltime < mClock.currentTimeMillis();
441        }
442
443        @Override
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
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