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