HeadsUpManager.java revision a59ecc3401de0c4bf1e13665158f54669f22d06c
1/*
2 * Copyright (C) 2011 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.policy;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.database.ContentObserver;
22import android.os.Handler;
23import android.os.SystemClock;
24import android.provider.Settings;
25import android.util.ArrayMap;
26import android.util.Log;
27import android.util.Pools;
28import android.view.View;
29import android.view.ViewTreeObserver;
30import android.view.accessibility.AccessibilityEvent;
31
32import com.android.systemui.R;
33import com.android.systemui.statusbar.ExpandableNotificationRow;
34import com.android.systemui.statusbar.NotificationData;
35import com.android.systemui.statusbar.phone.PhoneStatusBar;
36
37import java.io.FileDescriptor;
38import java.io.PrintWriter;
39import java.util.HashMap;
40import java.util.HashSet;
41import java.util.Stack;
42import java.util.TreeSet;
43
44public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener {
45    private static final String TAG = "HeadsUpManager";
46    private static final boolean DEBUG = false;
47    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
48
49    private final int mHeadsUpNotificationDecay;
50    private final int mMinimumDisplayTime;
51
52    private final int mTouchSensitivityDelay;
53    private final ArrayMap<String, Long> mSnoozedPackages;
54    private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
55    private final int mDefaultSnoozeLengthMs;
56    private final Handler mHandler = new Handler();
57    private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
58
59        private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
60
61        @Override
62        public HeadsUpEntry acquire() {
63            if (!mPoolObjects.isEmpty()) {
64                return mPoolObjects.pop();
65            }
66            return new HeadsUpEntry();
67        }
68
69        @Override
70        public boolean release(HeadsUpEntry instance) {
71            instance.removeAutoCancelCallbacks();
72            mPoolObjects.push(instance);
73            return true;
74        }
75    };
76
77
78    private PhoneStatusBar mBar;
79    private int mSnoozeLengthMs;
80    private ContentObserver mSettingsObserver;
81    private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
82    private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>();
83    private HashSet<String> mSwipedOutKeys = new HashSet<>();
84    private int mUser;
85    private Clock mClock;
86    private boolean mReleaseOnExpandFinish;
87    private boolean mTrackingHeadsUp;
88    private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
89    private boolean mIsExpanded;
90    private boolean mHasPinnedHeadsUp;
91    private int[] mTmpTwoArray = new int[2];
92
93    public HeadsUpManager(final Context context, ViewTreeObserver observer) {
94        Resources resources = context.getResources();
95        mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
96        if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
97        mSnoozedPackages = new ArrayMap<>();
98        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
99        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
100        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
101        mHeadsUpNotificationDecay = 200000000/*resources.getInteger(R.integer.heads_up_notification_decay)*/;;
102        mClock = new Clock();
103        // TODO: shadow mSwipeHelper.setMaxSwipeProgress(mMaxAlpha);
104
105        mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
106                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
107        mSettingsObserver = new ContentObserver(mHandler) {
108            @Override
109            public void onChange(boolean selfChange) {
110                final int packageSnoozeLengthMs = Settings.Global.getInt(
111                        context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
112                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
113                    mSnoozeLengthMs = packageSnoozeLengthMs;
114                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
115                }
116            }
117        };
118        context.getContentResolver().registerContentObserver(
119                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
120                mSettingsObserver);
121        if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
122        observer.addOnComputeInternalInsetsListener(this);
123    }
124
125    public void setBar(PhoneStatusBar bar) {
126        mBar = bar;
127    }
128
129    public void addListener(OnHeadsUpChangedListener listener) {
130        mListeners.add(listener);
131    }
132
133    public PhoneStatusBar getBar() {
134        return mBar;
135    }
136
137    /**
138     * Called when posting a new notification to the heads up.
139     */
140    public void showNotification(NotificationData.Entry headsUp) {
141        if (DEBUG) Log.v(TAG, "showNotification");
142        addHeadsUpEntry(headsUp);
143        updateNotification(headsUp, true);
144        headsUp.setInterruption();
145    }
146
147    /**
148     * Called when updating or posting a notification to the heads up.
149     */
150    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
151        if (DEBUG) Log.v(TAG, "updateNotification");
152
153        headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
154        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
155
156        if (alert) {
157            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
158            headsUpEntry.updateEntry();
159            headsUpEntry.entry.row.setInShade(mIsExpanded);
160        }
161    }
162
163    private void addHeadsUpEntry(NotificationData.Entry entry) {
164        HeadsUpEntry headsUpEntry = mEntryPool.acquire();
165
166        // This will also add the entry to the sortedList
167        headsUpEntry.setEntry(entry);
168        mHeadsUpEntries.put(entry.key, headsUpEntry);
169        entry.row.setHeadsUp(true);
170        if (!entry.row.isInShade() && mIsExpanded) {
171            headsUpEntry.entry.row.setInShade(true);
172        }
173        updatePinnedHeadsUpState(false);
174        for (OnHeadsUpChangedListener listener : mListeners) {
175            listener.OnHeadsUpStateChanged(entry, true);
176        }
177        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
178    }
179
180    private void removeHeadsUpEntry(NotificationData.Entry entry) {
181        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
182        mSortedEntries.remove(remove);
183        mEntryPool.release(remove);
184        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
185        entry.row.setHeadsUp(false);
186        updatePinnedHeadsUpState(false);
187        for (OnHeadsUpChangedListener listener : mListeners) {
188            listener.OnHeadsUpStateChanged(entry, false);
189        }
190    }
191
192    private void updatePinnedHeadsUpState(boolean forceImmediate) {
193        boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal();
194        if (hasPinnedHeadsUp == mHasPinnedHeadsUp) {
195            return;
196        }
197        mHasPinnedHeadsUp = hasPinnedHeadsUp;
198        for (OnHeadsUpChangedListener listener :mListeners) {
199            listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate);
200        }
201    }
202
203    /**
204     * React to the removal of the notification in the heads up.
205     *
206     * @return true if the notification was removed and false if it still needs to be kept around
207     * for a bit since it wasn't shown long enough
208     */
209    public boolean removeNotification(String key) {
210        if (DEBUG) Log.v(TAG, "remove");
211        if (wasShownLongEnough(key)) {
212            releaseImmediately(key);
213            return true;
214        } else {
215            getHeadsUpEntry(key).hideAsSoonAsPossible();
216            return false;
217        }
218    }
219
220    private boolean wasShownLongEnough(String key) {
221        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
222        HeadsUpEntry topEntry = getTopEntry();
223        if (mSwipedOutKeys.contains(key)) {
224            // We always instantly dismiss views being manually swiped out.
225            mSwipedOutKeys.remove(key);
226            return true;
227        }
228        if (headsUpEntry != topEntry) {
229            return true;
230        }
231        return headsUpEntry.wasShownLongEnough();
232    }
233
234    public boolean isHeadsUp(String key) {
235        return mHeadsUpEntries.containsKey(key);
236    }
237
238
239    /**
240     * Push any current Heads Up notification down into the shade.
241     */
242    public void releaseAllImmediately() {
243        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
244        HashSet<String> keys = new HashSet<>(mHeadsUpEntries.keySet());
245        for (String key: keys) {
246            releaseImmediately(key);
247        }
248    }
249
250    public void releaseImmediately(String key) {
251        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
252        if (headsUpEntry == null) {
253            return;
254        }
255        NotificationData.Entry shadeEntry = headsUpEntry.entry;
256        removeHeadsUpEntry(shadeEntry);
257    }
258
259    public boolean isSnoozed(String packageName) {
260        final String key = snoozeKey(packageName, mUser);
261        Long snoozedUntil = mSnoozedPackages.get(key);
262        if (snoozedUntil != null) {
263            if (snoozedUntil > SystemClock.elapsedRealtime()) {
264                if (DEBUG) Log.v(TAG, key + " snoozed");
265                return true;
266            }
267            mSnoozedPackages.remove(packageName);
268        }
269        return false;
270    }
271
272    public void snooze() {
273        for (String key: mHeadsUpEntries.keySet()) {
274            HeadsUpEntry entry = mHeadsUpEntries.get(key);
275            String packageName = entry.entry.notification.getPackageName();
276            mSnoozedPackages.put(snoozeKey(packageName, mUser),
277                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
278        }
279        mReleaseOnExpandFinish = true;
280    }
281
282    private static String snoozeKey(String packageName, int user) {
283        return user + "," + packageName;
284    }
285
286    private HeadsUpEntry getHeadsUpEntry(String key) {
287        return mHeadsUpEntries.get(key);
288    }
289
290    public NotificationData.Entry getEntry(String key) {
291        return mHeadsUpEntries.get(key).entry;
292    }
293
294    public TreeSet<HeadsUpEntry> getSortedEntries() {
295        return mSortedEntries;
296    }
297
298    public HeadsUpEntry getTopEntry() {
299        return mSortedEntries.isEmpty() ? null : mSortedEntries.first();
300    }
301
302    /**
303     * @param key the key of the touched notification
304     * @return whether the touch is valid and should not be discarded
305     */
306    public boolean shouldSwallowClick(String key) {
307        if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) {
308            return true;
309        }
310        return false;
311    }
312
313    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
314        // TODO: handle the shadow
315        //getBackground().setAlpha((int) (255 * swipeProgress));
316        return false;
317    }
318
319    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
320        if (!mIsExpanded && mHasPinnedHeadsUp) {
321            int minX = Integer.MAX_VALUE;
322            int maxX = 0;
323            int minY = Integer.MAX_VALUE;
324            int maxY = 0;
325            for (HeadsUpEntry entry: mSortedEntries) {
326                ExpandableNotificationRow row = entry.entry.row;
327                if (!row.isInShade()) {
328                    row.getLocationOnScreen(mTmpTwoArray);
329                    minX = Math.min(minX, mTmpTwoArray[0]);
330                    minY = Math.min(minY, 0);
331                    maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth());
332                    maxY = Math.max(maxY, row.getHeadsUpHeight());
333                }
334            }
335
336            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
337            info.touchableRegion.set(minX, minY, maxX, maxY);
338        }
339    }
340
341    public void setUser(int user) {
342        mUser = user;
343    }
344
345    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
346        pw.println("HeadsUpManager state:");
347        pw.print("  mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
348        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
349        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
350        pw.print("  mUser="); pw.println(mUser);
351        for (HeadsUpEntry entry: mSortedEntries) {
352            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
353        }
354        int N = mSnoozedPackages.size();
355        pw.println("  snoozed packages: " + N);
356        for (int i = 0; i < N; i++) {
357            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
358            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
359        }
360    }
361
362    public boolean hasPinnedHeadsUp() {
363        return mHasPinnedHeadsUp;
364    }
365
366    private boolean hasPinnedHeadsUpInternal() {
367        for (String key: mHeadsUpEntries.keySet()) {
368            HeadsUpEntry entry = mHeadsUpEntries.get(key);
369            if (!entry.entry.row.isInShade()) {
370                return true;
371            }
372        }
373        return false;
374    }
375
376    public void addSwipedOutKey(String key) {
377        mSwipedOutKeys.add(key);
378    }
379
380    public float getHighestPinnedHeadsUp() {
381        float max = 0;
382        for (HeadsUpEntry entry: mSortedEntries) {
383            if (!entry.entry.row.isInShade()) {
384                max = Math.max(max, entry.entry.row.getActualHeight());
385            }
386        }
387        return max;
388    }
389
390    public void releaseAllToShade() {
391        for (String key: mHeadsUpEntries.keySet()) {
392            HeadsUpEntry entry = mHeadsUpEntries.get(key);
393            entry.entry.row.setInShade(true);
394        }
395        updatePinnedHeadsUpState(true);
396    }
397
398    public void onExpandingFinished() {
399        if (mReleaseOnExpandFinish) {
400            releaseAllImmediately();
401            mReleaseOnExpandFinish = false;
402        } else {
403            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
404                removeHeadsUpEntry(entry);
405            }
406            mEntriesToRemoveAfterExpand.clear();
407        }
408    }
409
410    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
411        mTrackingHeadsUp = trackingHeadsUp;
412    }
413
414    public void setIsExpanded(boolean isExpanded) {
415        mIsExpanded = isExpanded;
416    }
417
418    public int getTopHeadsUpHeight() {
419        HeadsUpEntry topEntry = getTopEntry();
420        return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
421    }
422
423    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
424        public NotificationData.Entry entry;
425        public long postTime;
426        public long earliestRemovaltime;
427        private Runnable mRemoveHeadsUpRunnable;
428
429        public void setEntry(final NotificationData.Entry entry) {
430            this.entry = entry;
431
432            // The actual post time will be just after the heads-up really slided in
433            postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
434            mRemoveHeadsUpRunnable = new Runnable() {
435                @Override
436                public void run() {
437                    if (!mTrackingHeadsUp) {
438                        removeHeadsUpEntry(entry);
439                    } else {
440                        mEntriesToRemoveAfterExpand.add(entry);
441                    }
442                }
443            };
444            updateEntry();
445        }
446
447        public void updateEntry() {
448            long currentTime = mClock.currentTimeMillis();
449            postTime = Math.max(postTime, currentTime);
450            long finishTime = postTime + mHeadsUpNotificationDecay;
451            long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
452            earliestRemovaltime = currentTime + mMinimumDisplayTime;
453            removeAutoCancelCallbacks();
454            mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
455            updateSortOrder(HeadsUpEntry.this);
456        }
457
458        @Override
459        public int compareTo(HeadsUpEntry o) {
460            return postTime < o.postTime ? 1
461                    : postTime == o.postTime ? 0
462                            : -1;
463        }
464
465        public void removeAutoCancelCallbacks() {
466            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
467        }
468
469        public boolean wasShownLongEnough() {
470            return earliestRemovaltime < mClock.currentTimeMillis();
471        }
472
473        public void hideAsSoonAsPossible() {
474            removeAutoCancelCallbacks();
475            mHandler.postDelayed(mRemoveHeadsUpRunnable,
476                    earliestRemovaltime - mClock.currentTimeMillis());
477        }
478    }
479
480    /**
481     * Update the sorted heads up order.
482     *
483     * @param headsUpEntry the headsUp that changed
484     */
485    private void updateSortOrder(HeadsUpEntry headsUpEntry) {
486        mSortedEntries.remove(headsUpEntry);
487        mSortedEntries.add(headsUpEntry);
488    }
489
490    public static class Clock {
491        public long currentTimeMillis() {
492            return SystemClock.elapsedRealtime();
493        }
494    }
495
496    public interface OnHeadsUpChangedListener {
497        void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly);
498        void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
499    }
500}
501