• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.policy;
18 
19 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.Notification;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.database.ContentObserver;
27 import android.provider.Settings;
28 import android.util.ArrayMap;
29 import android.util.Log;
30 import android.view.accessibility.AccessibilityManager;
31 
32 import com.android.internal.logging.MetricsLogger;
33 import com.android.systemui.Dependency;
34 import com.android.systemui.R;
35 import com.android.systemui.statusbar.AlertingNotificationManager;
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
37 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
38 
39 import java.io.FileDescriptor;
40 import java.io.PrintWriter;
41 import java.util.HashSet;
42 
43 /**
44  * A manager which handles heads up notifications which is a special mode where
45  * they simply peek from the top of the screen.
46  */
47 public abstract class HeadsUpManager extends AlertingNotificationManager {
48     private static final String TAG = "HeadsUpManager";
49     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
50 
51     protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
52 
53     protected final Context mContext;
54 
55     protected int mTouchAcceptanceDelay;
56     protected int mSnoozeLengthMs;
57     protected boolean mHasPinnedNotification;
58     protected int mUser;
59 
60     private final ArrayMap<String, Long> mSnoozedPackages;
61     private final AccessibilityManagerWrapper mAccessibilityMgr;
62 
HeadsUpManager(@onNull final Context context)63     public HeadsUpManager(@NonNull final Context context) {
64         mContext = context;
65         mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
66         Resources resources = context.getResources();
67         mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
68         mAutoDismissNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
69         mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
70         mSnoozedPackages = new ArrayMap<>();
71         int defaultSnoozeLengthMs =
72                 resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
73 
74         mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
75                 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, defaultSnoozeLengthMs);
76         ContentObserver settingsObserver = new ContentObserver(mHandler) {
77             @Override
78             public void onChange(boolean selfChange) {
79                 final int packageSnoozeLengthMs = Settings.Global.getInt(
80                         context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
81                 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
82                     mSnoozeLengthMs = packageSnoozeLengthMs;
83                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
84                         Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
85                     }
86                 }
87             }
88         };
89         context.getContentResolver().registerContentObserver(
90                 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
91                 settingsObserver);
92     }
93 
94     /**
95      * Adds an OnHeadUpChangedListener to observe events.
96      */
addListener(@onNull OnHeadsUpChangedListener listener)97     public void addListener(@NonNull OnHeadsUpChangedListener listener) {
98         mListeners.add(listener);
99     }
100 
101     /**
102      * Removes the OnHeadUpChangedListener from the observer list.
103      */
removeListener(@onNull OnHeadsUpChangedListener listener)104     public void removeListener(@NonNull OnHeadsUpChangedListener listener) {
105         mListeners.remove(listener);
106     }
107 
updateNotification(@onNull String key, boolean alert)108     public void updateNotification(@NonNull String key, boolean alert) {
109         super.updateNotification(key, alert);
110         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
111         if (alert && headsUpEntry != null) {
112             setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry));
113         }
114     }
115 
shouldHeadsUpBecomePinned(@onNull NotificationEntry entry)116     protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) {
117         return hasFullScreenIntent(entry);
118     }
119 
hasFullScreenIntent(@onNull NotificationEntry entry)120     protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) {
121         return entry.getSbn().getNotification().fullScreenIntent != null;
122     }
123 
setEntryPinned( @onNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned)124     protected void setEntryPinned(
125             @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
126         if (Log.isLoggable(TAG, Log.VERBOSE)) {
127             Log.v(TAG, "setEntryPinned: " + isPinned);
128         }
129         NotificationEntry entry = headsUpEntry.mEntry;
130         if (entry.isRowPinned() != isPinned) {
131             entry.setRowPinned(isPinned);
132             updatePinnedMode();
133             for (OnHeadsUpChangedListener listener : mListeners) {
134                 if (isPinned) {
135                     listener.onHeadsUpPinned(entry);
136                 } else {
137                     listener.onHeadsUpUnPinned(entry);
138                 }
139             }
140         }
141     }
142 
getContentFlag()143     public @InflationFlag int getContentFlag() {
144         return FLAG_CONTENT_VIEW_HEADS_UP;
145     }
146 
147     @Override
onAlertEntryAdded(AlertEntry alertEntry)148     protected void onAlertEntryAdded(AlertEntry alertEntry) {
149         NotificationEntry entry = alertEntry.mEntry;
150         entry.setHeadsUp(true);
151         setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry));
152         for (OnHeadsUpChangedListener listener : mListeners) {
153             listener.onHeadsUpStateChanged(entry, true);
154         }
155     }
156 
157     @Override
onAlertEntryRemoved(AlertEntry alertEntry)158     protected void onAlertEntryRemoved(AlertEntry alertEntry) {
159         NotificationEntry entry = alertEntry.mEntry;
160         entry.setHeadsUp(false);
161         setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */);
162         for (OnHeadsUpChangedListener listener : mListeners) {
163             listener.onHeadsUpStateChanged(entry, false);
164         }
165     }
166 
updatePinnedMode()167     protected void updatePinnedMode() {
168         boolean hasPinnedNotification = hasPinnedNotificationInternal();
169         if (hasPinnedNotification == mHasPinnedNotification) {
170             return;
171         }
172         if (Log.isLoggable(TAG, Log.VERBOSE)) {
173             Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " +
174                        hasPinnedNotification);
175         }
176         mHasPinnedNotification = hasPinnedNotification;
177         if (mHasPinnedNotification) {
178             MetricsLogger.count(mContext, "note_peek", 1);
179         }
180         for (OnHeadsUpChangedListener listener : mListeners) {
181             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
182         }
183     }
184 
185     /**
186      * Returns if the given notification is snoozed or not.
187      */
isSnoozed(@onNull String packageName)188     public boolean isSnoozed(@NonNull String packageName) {
189         final String key = snoozeKey(packageName, mUser);
190         Long snoozedUntil = mSnoozedPackages.get(key);
191         if (snoozedUntil != null) {
192             if (snoozedUntil > mClock.currentTimeMillis()) {
193                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
194                     Log.v(TAG, key + " snoozed");
195                 }
196                 return true;
197             }
198             mSnoozedPackages.remove(packageName);
199         }
200         return false;
201     }
202 
203     /**
204      * Snoozes all current Heads Up Notifications.
205      */
snooze()206     public void snooze() {
207         for (String key : mAlertEntries.keySet()) {
208             AlertEntry entry = getHeadsUpEntry(key);
209             String packageName = entry.mEntry.getSbn().getPackageName();
210             mSnoozedPackages.put(snoozeKey(packageName, mUser),
211                     mClock.currentTimeMillis() + mSnoozeLengthMs);
212         }
213     }
214 
215     @NonNull
snoozeKey(@onNull String packageName, int user)216     private static String snoozeKey(@NonNull String packageName, int user) {
217         return user + "," + packageName;
218     }
219 
220     @Nullable
getHeadsUpEntry(@onNull String key)221     protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
222         return (HeadsUpEntry) mAlertEntries.get(key);
223     }
224 
225     /**
226      * Returns the top Heads Up Notification, which appears to show at first.
227      */
228     @Nullable
getTopEntry()229     public NotificationEntry getTopEntry() {
230         HeadsUpEntry topEntry = getTopHeadsUpEntry();
231         return (topEntry != null) ? topEntry.mEntry : null;
232     }
233 
234     @Nullable
getTopHeadsUpEntry()235     protected HeadsUpEntry getTopHeadsUpEntry() {
236         if (mAlertEntries.isEmpty()) {
237             return null;
238         }
239         HeadsUpEntry topEntry = null;
240         for (AlertEntry entry: mAlertEntries.values()) {
241             if (topEntry == null || entry.compareTo(topEntry) < 0) {
242                 topEntry = (HeadsUpEntry) entry;
243             }
244         }
245         return topEntry;
246     }
247 
248     /**
249      * Sets the current user.
250      */
setUser(int user)251     public void setUser(int user) {
252         mUser = user;
253     }
254 
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)255     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
256         pw.println("HeadsUpManager state:");
257         dumpInternal(fd, pw, args);
258     }
259 
dumpInternal( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)260     protected void dumpInternal(
261             @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
262         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
263         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
264         pw.print("  now="); pw.println(mClock.currentTimeMillis());
265         pw.print("  mUser="); pw.println(mUser);
266         for (AlertEntry entry: mAlertEntries.values()) {
267             pw.print("  HeadsUpEntry="); pw.println(entry.mEntry);
268         }
269         int N = mSnoozedPackages.size();
270         pw.println("  snoozed packages: " + N);
271         for (int i = 0; i < N; i++) {
272             pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
273             pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
274         }
275     }
276 
277     /**
278      * Returns if there are any pinned Heads Up Notifications or not.
279      */
hasPinnedHeadsUp()280     public boolean hasPinnedHeadsUp() {
281         return mHasPinnedNotification;
282     }
283 
hasPinnedNotificationInternal()284     private boolean hasPinnedNotificationInternal() {
285         for (String key : mAlertEntries.keySet()) {
286             AlertEntry entry = getHeadsUpEntry(key);
287             if (entry.mEntry.isRowPinned()) {
288                 return true;
289             }
290         }
291         return false;
292     }
293 
294     /**
295      * Unpins all pinned Heads Up Notifications.
296      * @param userUnPinned The unpinned action is trigger by user real operation.
297      */
unpinAll(boolean userUnPinned)298     public void unpinAll(boolean userUnPinned) {
299         for (String key : mAlertEntries.keySet()) {
300             HeadsUpEntry entry = getHeadsUpEntry(key);
301             setEntryPinned(entry, false /* isPinned */);
302             // maybe it got un sticky
303             entry.updateEntry(false /* updatePostTime */);
304 
305             // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
306             // on the screen.
307             if (userUnPinned && entry.mEntry != null) {
308                 if (entry.mEntry.mustStayOnScreen()) {
309                     entry.mEntry.setHeadsUpIsVisible();
310                 }
311             }
312         }
313     }
314 
315     /**
316      * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as
317      * well.
318      */
isTrackingHeadsUp()319     public boolean isTrackingHeadsUp() {
320         // Might be implemented in subclass.
321         return false;
322     }
323 
324     /**
325      * Compare two entries and decide how they should be ranked.
326      *
327      * @return -1 if the first argument should be ranked higher than the second, 1 if the second
328      * one should be ranked higher and 0 if they are equal.
329      */
compare(@onNull NotificationEntry a, @NonNull NotificationEntry b)330     public int compare(@NonNull NotificationEntry a, @NonNull NotificationEntry b) {
331         AlertEntry aEntry = getHeadsUpEntry(a.getKey());
332         AlertEntry bEntry = getHeadsUpEntry(b.getKey());
333         if (aEntry == null || bEntry == null) {
334             return aEntry == null ? 1 : -1;
335         }
336         return aEntry.compareTo(bEntry);
337     }
338 
339     /**
340      * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
341      * until it's collapsed again.
342      */
setExpanded(@onNull NotificationEntry entry, boolean expanded)343     public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) {
344         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
345         if (headsUpEntry != null && entry.isRowPinned()) {
346             headsUpEntry.setExpanded(expanded);
347         }
348     }
349 
350     @NonNull
351     @Override
createAlertEntry()352     protected HeadsUpEntry createAlertEntry() {
353         return new HeadsUpEntry();
354     }
355 
onDensityOrFontScaleChanged()356     public void onDensityOrFontScaleChanged() {
357     }
358 
isEntryAutoHeadsUpped(String key)359     public boolean isEntryAutoHeadsUpped(String key) {
360         return false;
361     }
362 
isOngoingCallNotif(NotificationEntry entry)363     private static boolean isOngoingCallNotif(NotificationEntry entry) {
364         return entry.getSbn().isOngoing() && Notification.CATEGORY_CALL.equals(
365                 entry.getSbn().getNotification().category);
366     }
367 
368     /**
369      * This represents a notification and how long it is in a heads up mode. It also manages its
370      * lifecycle automatically when created.
371      */
372     protected class HeadsUpEntry extends AlertEntry {
373         public boolean remoteInputActive;
374         protected boolean expanded;
375 
376         @Override
isSticky()377         public boolean isSticky() {
378             return (mEntry.isRowPinned() && expanded)
379                     || remoteInputActive || hasFullScreenIntent(mEntry);
380         }
381 
382         @Override
compareTo(@onNull AlertEntry alertEntry)383         public int compareTo(@NonNull AlertEntry alertEntry) {
384             HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry;
385             boolean isPinned = mEntry.isRowPinned();
386             boolean otherPinned = headsUpEntry.mEntry.isRowPinned();
387             if (isPinned && !otherPinned) {
388                 return -1;
389             } else if (!isPinned && otherPinned) {
390                 return 1;
391             }
392             boolean selfFullscreen = hasFullScreenIntent(mEntry);
393             boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry);
394             if (selfFullscreen && !otherFullscreen) {
395                 return -1;
396             } else if (!selfFullscreen && otherFullscreen) {
397                 return 1;
398             }
399 
400             boolean selfCall = isOngoingCallNotif(mEntry);
401             boolean otherCall = isOngoingCallNotif(headsUpEntry.mEntry);
402 
403             if (selfCall && !otherCall) {
404                 return -1;
405             } else if (!selfCall && otherCall) {
406                 return 1;
407             }
408 
409             if (remoteInputActive && !headsUpEntry.remoteInputActive) {
410                 return -1;
411             } else if (!remoteInputActive && headsUpEntry.remoteInputActive) {
412                 return 1;
413             }
414 
415             return super.compareTo(headsUpEntry);
416         }
417 
setExpanded(boolean expanded)418         public void setExpanded(boolean expanded) {
419             this.expanded = expanded;
420         }
421 
422         @Override
reset()423         public void reset() {
424             super.reset();
425             expanded = false;
426             remoteInputActive = false;
427         }
428 
429         @Override
calculatePostTime()430         protected long calculatePostTime() {
431             // The actual post time will be just after the heads-up really slided in
432             return super.calculatePostTime() + mTouchAcceptanceDelay;
433         }
434 
435         @Override
calculateFinishTime()436         protected long calculateFinishTime() {
437             return mPostTime + getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay);
438         }
439 
440         /**
441          * Get user-preferred or default timeout duration. The larger one will be returned.
442          * @return milliseconds before auto-dismiss
443          * @param requestedTimeout
444          */
getRecommendedHeadsUpTimeoutMs(int requestedTimeout)445         protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) {
446             return mAccessibilityMgr.getRecommendedTimeoutMillis(
447                     requestedTimeout,
448                     AccessibilityManager.FLAG_CONTENT_CONTROLS
449                             | AccessibilityManager.FLAG_CONTENT_ICONS
450                             | AccessibilityManager.FLAG_CONTENT_TEXT);
451         }
452     }
453 }
454