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