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