HeadsUpManager.java revision fbe9a44a15addf9d94cd40da56835501241b8d3e
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            setEntryToShade(headsUpEntry, 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            setEntryToShade(headsUpEntry, true);
172        }
173        updatePinnedHeadsUpState(false /*forceImmediate */);
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 setEntryToShade(HeadsUpEntry headsUpEntry, boolean inShade) {
181        ExpandableNotificationRow row = headsUpEntry.entry.row;
182        if (row.isInShade() != inShade) {
183            row.setInShade(inShade);
184            updatePinnedHeadsUpState(false /* forceImmediate */);
185            if (!inShade) {
186                for (OnHeadsUpChangedListener listener :mListeners) {
187                    listener.OnHeadsUpPinned(row);
188                }
189            }
190        }
191    }
192
193    private void removeHeadsUpEntry(NotificationData.Entry entry) {
194        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
195        mSortedEntries.remove(remove);
196        mEntryPool.release(remove);
197        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
198        entry.row.setHeadsUp(false);
199        updatePinnedHeadsUpState(false /* forceImmediate */);
200        for (OnHeadsUpChangedListener listener : mListeners) {
201            listener.OnHeadsUpStateChanged(entry, false);
202        }
203    }
204
205    private void updatePinnedHeadsUpState(boolean forceImmediate) {
206        boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal();
207        if (hasPinnedHeadsUp == mHasPinnedHeadsUp) {
208            return;
209        }
210        mHasPinnedHeadsUp = hasPinnedHeadsUp;
211        for (OnHeadsUpChangedListener listener :mListeners) {
212            listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate);
213        }
214    }
215
216    /**
217     * React to the removal of the notification in the heads up.
218     *
219     * @return true if the notification was removed and false if it still needs to be kept around
220     * for a bit since it wasn't shown long enough
221     */
222    public boolean removeNotification(String key) {
223        if (DEBUG) Log.v(TAG, "remove");
224        if (wasShownLongEnough(key)) {
225            releaseImmediately(key);
226            return true;
227        } else {
228            getHeadsUpEntry(key).hideAsSoonAsPossible();
229            return false;
230        }
231    }
232
233    private boolean wasShownLongEnough(String key) {
234        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
235        HeadsUpEntry topEntry = getTopEntry();
236        if (mSwipedOutKeys.contains(key)) {
237            // We always instantly dismiss views being manually swiped out.
238            mSwipedOutKeys.remove(key);
239            return true;
240        }
241        if (headsUpEntry != topEntry) {
242            return true;
243        }
244        return headsUpEntry.wasShownLongEnough();
245    }
246
247    public boolean isHeadsUp(String key) {
248        return mHeadsUpEntries.containsKey(key);
249    }
250
251
252    /**
253     * Push any current Heads Up notification down into the shade.
254     */
255    public void releaseAllImmediately() {
256        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
257        HashSet<String> keys = new HashSet<>(mHeadsUpEntries.keySet());
258        for (String key: keys) {
259            releaseImmediately(key);
260        }
261    }
262
263    public void releaseImmediately(String key) {
264        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
265        if (headsUpEntry == null) {
266            return;
267        }
268        NotificationData.Entry shadeEntry = headsUpEntry.entry;
269        removeHeadsUpEntry(shadeEntry);
270    }
271
272    public boolean isSnoozed(String packageName) {
273        final String key = snoozeKey(packageName, mUser);
274        Long snoozedUntil = mSnoozedPackages.get(key);
275        if (snoozedUntil != null) {
276            if (snoozedUntil > SystemClock.elapsedRealtime()) {
277                if (DEBUG) Log.v(TAG, key + " snoozed");
278                return true;
279            }
280            mSnoozedPackages.remove(packageName);
281        }
282        return false;
283    }
284
285    public void snooze() {
286        for (String key: mHeadsUpEntries.keySet()) {
287            HeadsUpEntry entry = mHeadsUpEntries.get(key);
288            String packageName = entry.entry.notification.getPackageName();
289            mSnoozedPackages.put(snoozeKey(packageName, mUser),
290                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
291        }
292        mReleaseOnExpandFinish = true;
293    }
294
295    private static String snoozeKey(String packageName, int user) {
296        return user + "," + packageName;
297    }
298
299    private HeadsUpEntry getHeadsUpEntry(String key) {
300        return mHeadsUpEntries.get(key);
301    }
302
303    public NotificationData.Entry getEntry(String key) {
304        return mHeadsUpEntries.get(key).entry;
305    }
306
307    public TreeSet<HeadsUpEntry> getSortedEntries() {
308        return mSortedEntries;
309    }
310
311    public HeadsUpEntry getTopEntry() {
312        return mSortedEntries.isEmpty() ? null : mSortedEntries.first();
313    }
314
315    /**
316     * @param key the key of the touched notification
317     * @return whether the touch is valid and should not be discarded
318     */
319    public boolean shouldSwallowClick(String key) {
320        if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) {
321            return true;
322        }
323        return false;
324    }
325
326    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
327        // TODO: handle the shadow
328        //getBackground().setAlpha((int) (255 * swipeProgress));
329        return false;
330    }
331
332    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
333        if (!mIsExpanded && mHasPinnedHeadsUp) {
334            int minX = Integer.MAX_VALUE;
335            int maxX = 0;
336            int minY = Integer.MAX_VALUE;
337            int maxY = 0;
338            for (HeadsUpEntry entry: mSortedEntries) {
339                ExpandableNotificationRow row = entry.entry.row;
340                if (!row.isInShade()) {
341                    row.getLocationOnScreen(mTmpTwoArray);
342                    minX = Math.min(minX, mTmpTwoArray[0]);
343                    minY = Math.min(minY, 0);
344                    maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth());
345                    maxY = Math.max(maxY, row.getHeadsUpHeight());
346                }
347            }
348
349            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
350            info.touchableRegion.set(minX, minY, maxX, maxY);
351        }
352    }
353
354    public void setUser(int user) {
355        mUser = user;
356    }
357
358    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
359        pw.println("HeadsUpManager state:");
360        pw.print("  mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
361        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
362        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
363        pw.print("  mUser="); pw.println(mUser);
364        for (HeadsUpEntry entry: mSortedEntries) {
365            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
366        }
367        int N = mSnoozedPackages.size();
368        pw.println("  snoozed packages: " + N);
369        for (int i = 0; i < N; i++) {
370            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
371            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
372        }
373    }
374
375    public boolean hasPinnedHeadsUp() {
376        return mHasPinnedHeadsUp;
377    }
378
379    private boolean hasPinnedHeadsUpInternal() {
380        for (String key: mHeadsUpEntries.keySet()) {
381            HeadsUpEntry entry = mHeadsUpEntries.get(key);
382            if (!entry.entry.row.isInShade()) {
383                return true;
384            }
385        }
386        return false;
387    }
388
389    public void addSwipedOutKey(String key) {
390        mSwipedOutKeys.add(key);
391    }
392
393    public float getHighestPinnedHeadsUp() {
394        float max = 0;
395        for (HeadsUpEntry entry: mSortedEntries) {
396            if (!entry.entry.row.isInShade()) {
397                max = Math.max(max, entry.entry.row.getActualHeight());
398            }
399        }
400        return max;
401    }
402
403    public void releaseAllToShade() {
404        for (String key: mHeadsUpEntries.keySet()) {
405            HeadsUpEntry entry = mHeadsUpEntries.get(key);
406            entry.entry.row.setInShade(true);
407        }
408        updatePinnedHeadsUpState(true /* forceImmediate */);
409    }
410
411    public void onExpandingFinished() {
412        if (mReleaseOnExpandFinish) {
413            releaseAllImmediately();
414            mReleaseOnExpandFinish = false;
415        } else {
416            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
417                removeHeadsUpEntry(entry);
418            }
419            mEntriesToRemoveAfterExpand.clear();
420        }
421    }
422
423    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
424        mTrackingHeadsUp = trackingHeadsUp;
425    }
426
427    public void setIsExpanded(boolean isExpanded) {
428        if (isExpanded != mIsExpanded) {
429            mIsExpanded = isExpanded;
430            if (isExpanded) {
431                releaseAllToShade();
432            }
433        }
434    }
435
436    public int getTopHeadsUpHeight() {
437        HeadsUpEntry topEntry = getTopEntry();
438        return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
439    }
440
441    public int compare(NotificationData.Entry a, NotificationData.Entry b) {
442        HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
443        HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
444        if (aEntry == null || bEntry == null) {
445            return aEntry == null ? 1 : -1;
446        }
447        return aEntry.compareTo(bEntry);
448    }
449
450    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
451        public NotificationData.Entry entry;
452        public long postTime;
453        public long earliestRemovaltime;
454        private Runnable mRemoveHeadsUpRunnable;
455
456        public void setEntry(final NotificationData.Entry entry) {
457            this.entry = entry;
458
459            // The actual post time will be just after the heads-up really slided in
460            postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
461            mRemoveHeadsUpRunnable = new Runnable() {
462                @Override
463                public void run() {
464                    if (!mTrackingHeadsUp) {
465                        removeHeadsUpEntry(entry);
466                    } else {
467                        mEntriesToRemoveAfterExpand.add(entry);
468                    }
469                }
470            };
471            updateEntry();
472        }
473
474        public void updateEntry() {
475            long currentTime = mClock.currentTimeMillis();
476            postTime = Math.max(postTime, currentTime);
477            long finishTime = postTime + mHeadsUpNotificationDecay;
478            long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
479            earliestRemovaltime = currentTime + mMinimumDisplayTime;
480            removeAutoCancelCallbacks();
481            mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
482            updateSortOrder(HeadsUpEntry.this);
483        }
484
485        @Override
486        public int compareTo(HeadsUpEntry o) {
487            return postTime < o.postTime ? 1
488                    : postTime == o.postTime ? 0
489                            : -1;
490        }
491
492        public void removeAutoCancelCallbacks() {
493            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
494        }
495
496        public boolean wasShownLongEnough() {
497            return earliestRemovaltime < mClock.currentTimeMillis();
498        }
499
500        public void hideAsSoonAsPossible() {
501            removeAutoCancelCallbacks();
502            mHandler.postDelayed(mRemoveHeadsUpRunnable,
503                    earliestRemovaltime - mClock.currentTimeMillis());
504        }
505    }
506
507    /**
508     * Update the sorted heads up order.
509     *
510     * @param headsUpEntry the headsUp that changed
511     */
512    private void updateSortOrder(HeadsUpEntry headsUpEntry) {
513        mSortedEntries.remove(headsUpEntry);
514        mSortedEntries.add(headsUpEntry);
515    }
516
517    public static class Clock {
518        public long currentTimeMillis() {
519            return SystemClock.elapsedRealtime();
520        }
521    }
522
523    public interface OnHeadsUpChangedListener {
524        void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly);
525        void OnHeadsUpPinned(ExpandableNotificationRow headsUp);
526        void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
527    }
528}
529