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