1 /*
2  * Copyright (C) 2018 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.notification;
18 
19 import static com.android.systemui.statusbar.StatusBarState.SHADE;
20 
21 import android.app.Notification;
22 import android.app.NotificationManager;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.hardware.display.AmbientDisplayConfiguration;
26 import android.os.Build;
27 import android.os.PowerManager;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.os.UserHandle;
31 import android.provider.Settings;
32 import android.service.dreams.DreamService;
33 import android.service.dreams.IDreamManager;
34 import android.service.notification.StatusBarNotification;
35 import android.util.Log;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.systemui.Dependency;
39 import com.android.systemui.plugins.statusbar.StatusBarStateController;
40 import com.android.systemui.statusbar.NotificationPresenter;
41 import com.android.systemui.statusbar.StatusBarState;
42 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
43 import com.android.systemui.statusbar.policy.BatteryController;
44 import com.android.systemui.statusbar.policy.HeadsUpManager;
45 
46 import javax.inject.Inject;
47 import javax.inject.Singleton;
48 
49 /**
50  * Provides heads-up and pulsing state for notification entries.
51  */
52 @Singleton
53 public class NotificationInterruptionStateProvider {
54 
55     private static final String TAG = "InterruptionStateProvider";
56     private static final boolean DEBUG = false;
57     private static final boolean DEBUG_HEADS_UP = Build.IS_DEBUGGABLE;
58     private static final boolean ENABLE_HEADS_UP = true;
59     private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
60 
61     private final StatusBarStateController mStatusBarStateController;
62     private final NotificationFilter mNotificationFilter;
63     private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
64 
65     private final Context mContext;
66     private final PowerManager mPowerManager;
67     private final IDreamManager mDreamManager;
68     private final BatteryController mBatteryController;
69 
70     private NotificationPresenter mPresenter;
71     private HeadsUpManager mHeadsUpManager;
72     private HeadsUpSuppressor mHeadsUpSuppressor;
73 
74     private ContentObserver mHeadsUpObserver;
75     @VisibleForTesting
76     protected boolean mUseHeadsUp = false;
77     private boolean mDisableNotificationAlerts;
78 
79     @Inject
NotificationInterruptionStateProvider(Context context, NotificationFilter filter, StatusBarStateController stateController, BatteryController batteryController)80     public NotificationInterruptionStateProvider(Context context, NotificationFilter filter,
81             StatusBarStateController stateController, BatteryController batteryController) {
82         this(context,
83                 (PowerManager) context.getSystemService(Context.POWER_SERVICE),
84                 IDreamManager.Stub.asInterface(
85                         ServiceManager.checkService(DreamService.DREAM_SERVICE)),
86                 new AmbientDisplayConfiguration(context),
87                 filter,
88                 batteryController,
89                 stateController);
90     }
91 
92     @VisibleForTesting
NotificationInterruptionStateProvider( Context context, PowerManager powerManager, IDreamManager dreamManager, AmbientDisplayConfiguration ambientDisplayConfiguration, NotificationFilter notificationFilter, BatteryController batteryController, StatusBarStateController statusBarStateController)93     protected NotificationInterruptionStateProvider(
94             Context context,
95             PowerManager powerManager,
96             IDreamManager dreamManager,
97             AmbientDisplayConfiguration ambientDisplayConfiguration,
98             NotificationFilter notificationFilter,
99             BatteryController batteryController,
100             StatusBarStateController statusBarStateController) {
101         mContext = context;
102         mPowerManager = powerManager;
103         mDreamManager = dreamManager;
104         mBatteryController = batteryController;
105         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
106         mNotificationFilter = notificationFilter;
107         mStatusBarStateController = statusBarStateController;
108     }
109 
110     /** Sets up late-binding dependencies for this component. */
setUpWithPresenter( NotificationPresenter notificationPresenter, HeadsUpManager headsUpManager, HeadsUpSuppressor headsUpSuppressor)111     public void setUpWithPresenter(
112             NotificationPresenter notificationPresenter,
113             HeadsUpManager headsUpManager,
114             HeadsUpSuppressor headsUpSuppressor) {
115         setUpWithPresenter(notificationPresenter, headsUpManager, headsUpSuppressor,
116                 new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) {
117                     @Override
118                     public void onChange(boolean selfChange) {
119                         boolean wasUsing = mUseHeadsUp;
120                         mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
121                                 && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
122                                 mContext.getContentResolver(),
123                                 Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
124                                 Settings.Global.HEADS_UP_OFF);
125                         Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
126                         if (wasUsing != mUseHeadsUp) {
127                             if (!mUseHeadsUp) {
128                                 Log.d(TAG,
129                                         "dismissing any existing heads up notification on disable"
130                                                 + " event");
131                                 mHeadsUpManager.releaseAllImmediately();
132                             }
133                         }
134                     }
135                 });
136     }
137 
138     /** Sets up late-binding dependencies for this component. */
setUpWithPresenter( NotificationPresenter notificationPresenter, HeadsUpManager headsUpManager, HeadsUpSuppressor headsUpSuppressor, ContentObserver observer)139     public void setUpWithPresenter(
140             NotificationPresenter notificationPresenter,
141             HeadsUpManager headsUpManager,
142             HeadsUpSuppressor headsUpSuppressor,
143             ContentObserver observer) {
144         mPresenter = notificationPresenter;
145         mHeadsUpManager = headsUpManager;
146         mHeadsUpSuppressor = headsUpSuppressor;
147         mHeadsUpObserver = observer;
148 
149         if (ENABLE_HEADS_UP) {
150             mContext.getContentResolver().registerContentObserver(
151                     Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
152                     true,
153                     mHeadsUpObserver);
154             mContext.getContentResolver().registerContentObserver(
155                     Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
156                     mHeadsUpObserver);
157         }
158         mHeadsUpObserver.onChange(true); // set up
159     }
160 
161     /**
162      * Whether the notification should appear as a bubble with a fly-out on top of the screen.
163      *
164      * @param entry the entry to check
165      * @return true if the entry should bubble up, false otherwise
166      */
shouldBubbleUp(NotificationEntry entry)167     public boolean shouldBubbleUp(NotificationEntry entry) {
168         final StatusBarNotification sbn = entry.notification;
169 
170         if (!canAlertCommon(entry)) {
171             return false;
172         }
173 
174         if (!canAlertAwakeCommon(entry)) {
175             return false;
176         }
177 
178         if (!entry.canBubble) {
179             if (DEBUG) {
180                 Log.d(TAG, "No bubble up: not allowed to bubble: " + sbn.getKey());
181             }
182             return false;
183         }
184 
185         if (!entry.isBubble()) {
186             if (DEBUG) {
187                 Log.d(TAG, "No bubble up: notification " + sbn.getKey()
188                         + " is bubble? " + entry.isBubble());
189             }
190             return false;
191         }
192 
193         final Notification n = sbn.getNotification();
194         if (n.getBubbleMetadata() == null || n.getBubbleMetadata().getIntent() == null) {
195             if (DEBUG) {
196                 Log.d(TAG, "No bubble up: notification: " + sbn.getKey()
197                         + " doesn't have valid metadata");
198             }
199             return false;
200         }
201 
202         return true;
203     }
204 
205     /**
206      * Whether the notification should peek in from the top and alert the user.
207      *
208      * @param entry the entry to check
209      * @return true if the entry should heads up, false otherwise
210      */
shouldHeadsUp(NotificationEntry entry)211     public boolean shouldHeadsUp(NotificationEntry entry) {
212         if (mStatusBarStateController.isDozing()) {
213             return shouldHeadsUpWhenDozing(entry);
214         } else {
215             return shouldHeadsUpWhenAwake(entry);
216         }
217     }
218 
shouldHeadsUpWhenAwake(NotificationEntry entry)219     private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) {
220         StatusBarNotification sbn = entry.notification;
221 
222         if (!mUseHeadsUp) {
223             if (DEBUG_HEADS_UP) {
224                 Log.d(TAG, "No heads up: no huns");
225             }
226             return false;
227         }
228 
229         if (!canAlertCommon(entry)) {
230             return false;
231         }
232 
233         if (!canAlertAwakeCommon(entry)) {
234             return false;
235         }
236 
237         boolean inShade = mStatusBarStateController.getState() == SHADE;
238         if (entry.isBubble() && inShade) {
239             if (DEBUG_HEADS_UP) {
240                 Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a "
241                         + "bubble: " + sbn.getKey());
242             }
243             return false;
244         }
245 
246         if (entry.shouldSuppressPeek()) {
247             if (DEBUG_HEADS_UP) {
248                 Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey());
249             }
250             return false;
251         }
252 
253         if (entry.importance < NotificationManager.IMPORTANCE_HIGH) {
254             if (DEBUG_HEADS_UP) {
255                 Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey());
256             }
257             return false;
258         }
259 
260         boolean isDreaming = false;
261         try {
262             isDreaming = mDreamManager.isDreaming();
263         } catch (RemoteException e) {
264             Log.e(TAG, "Failed to query dream manager.", e);
265         }
266         boolean inUse = mPowerManager.isScreenOn() && !isDreaming;
267 
268         if (!inUse) {
269             if (DEBUG_HEADS_UP) {
270                 Log.d(TAG, "No heads up: not in use: " + sbn.getKey());
271             }
272             return false;
273         }
274 
275         if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) {
276             if (DEBUG_HEADS_UP) {
277                 Log.d(TAG, "No heads up: aborted by suppressor: " + sbn.getKey());
278             }
279             return false;
280         }
281 
282         return true;
283     }
284 
285     /**
286      * Whether or not the notification should "pulse" on the user's display when the phone is
287      * dozing.  This displays the ambient view of the notification.
288      *
289      * @param entry the entry to check
290      * @return true if the entry should ambient pulse, false otherwise
291      */
shouldHeadsUpWhenDozing(NotificationEntry entry)292     private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) {
293         StatusBarNotification sbn = entry.notification;
294 
295         if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
296             if (DEBUG_HEADS_UP) {
297                 Log.d(TAG, "No pulsing: disabled by setting: " + sbn.getKey());
298             }
299             return false;
300         }
301 
302         if (mBatteryController.isAodPowerSave()) {
303             if (DEBUG_HEADS_UP) {
304                 Log.d(TAG, "No pulsing: disabled by battery saver: " + sbn.getKey());
305             }
306             return false;
307         }
308 
309         if (!canAlertCommon(entry)) {
310             if (DEBUG_HEADS_UP) {
311                 Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey());
312             }
313             return false;
314         }
315 
316         if (entry.shouldSuppressAmbient()) {
317             if (DEBUG_HEADS_UP) {
318                 Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey());
319             }
320             return false;
321         }
322 
323         if (entry.importance < NotificationManager.IMPORTANCE_DEFAULT) {
324             if (DEBUG_HEADS_UP) {
325                 Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey());
326             }
327             return false;
328         }
329          return true;
330     }
331 
332     /**
333      * Common checks between regular & AOD heads up and bubbles.
334      *
335      * @param entry the entry to check
336      * @return true if these checks pass, false if the notification should not alert
337      */
338     @VisibleForTesting
canAlertCommon(NotificationEntry entry)339     public boolean canAlertCommon(NotificationEntry entry) {
340         StatusBarNotification sbn = entry.notification;
341 
342         if (mNotificationFilter.shouldFilterOut(entry)) {
343             if (DEBUG || DEBUG_HEADS_UP) {
344                 Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey());
345             }
346             return false;
347         }
348 
349         // Don't alert notifications that are suppressed due to group alert behavior
350         if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
351             if (DEBUG || DEBUG_HEADS_UP) {
352                 Log.d(TAG, "No alerting: suppressed due to group alert behavior");
353             }
354             return false;
355         }
356         return true;
357     }
358 
359     /**
360      * Common checks between alerts that occur while the device is awake (heads up & bubbles).
361      *
362      * @param entry the entry to check
363      * @return true if these checks pass, false if the notification should not alert
364      */
365     @VisibleForTesting
canAlertAwakeCommon(NotificationEntry entry)366     public boolean canAlertAwakeCommon(NotificationEntry entry) {
367         StatusBarNotification sbn = entry.notification;
368 
369         if (mPresenter.isDeviceInVrMode()) {
370             if (DEBUG_HEADS_UP) {
371                 Log.d(TAG, "No alerting: no huns or vr mode");
372             }
373             return false;
374         }
375 
376         if (isSnoozedPackage(sbn)) {
377             if (DEBUG_HEADS_UP) {
378                 Log.d(TAG, "No alerting: snoozed package: " + sbn.getKey());
379             }
380             return false;
381         }
382 
383         if (entry.hasJustLaunchedFullScreenIntent()) {
384             if (DEBUG_HEADS_UP) {
385                 Log.d(TAG, "No alerting: recent fullscreen: " + sbn.getKey());
386             }
387             return false;
388         }
389 
390         return true;
391     }
392 
isSnoozedPackage(StatusBarNotification sbn)393     private boolean isSnoozedPackage(StatusBarNotification sbn) {
394         return mHeadsUpManager.isSnoozed(sbn.getPackageName());
395     }
396 
397     /** Sets whether to disable all alerts. */
setDisableNotificationAlerts(boolean disableNotificationAlerts)398     public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
399         mDisableNotificationAlerts = disableNotificationAlerts;
400         mHeadsUpObserver.onChange(true);
401     }
402 
403     /** Whether all alerts are disabled. */
404     @VisibleForTesting
areNotificationAlertsDisabled()405     public boolean areNotificationAlertsDisabled() {
406         return mDisableNotificationAlerts;
407     }
408 
409     /** Whether HUNs should be used. */
410     @VisibleForTesting
getUseHeadsUp()411     public boolean getUseHeadsUp() {
412         return mUseHeadsUp;
413     }
414 
getPresenter()415     protected NotificationPresenter getPresenter() {
416         return mPresenter;
417     }
418 
419     /**
420      * When an entry was added, should we launch its fullscreen intent? Examples are Alarms or
421      * incoming calls.
422      *
423      * @param entry the entry that was added
424      * @return {@code true} if we should launch the full screen intent
425      */
shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry)426     public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) {
427         return entry.notification.getNotification().fullScreenIntent != null
428             && (!shouldHeadsUp(entry)
429                 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD);
430     }
431 
432     /** A component which can suppress heads-up notifications due to the overall state of the UI. */
433     public interface HeadsUpSuppressor {
434         /**
435          * Returns false if the provided notification is ineligible for heads-up according to this
436          * component.
437          *
438          * @param entry entry of the notification that might be heads upped
439          * @param sbn   notification that might be heads upped
440          * @return false if the notification can not be heads upped
441          */
canHeadsUp(NotificationEntry entry, StatusBarNotification sbn)442         boolean canHeadsUp(NotificationEntry entry, StatusBarNotification sbn);
443 
444     }
445 
446 }
447