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