1 /*
2  * Copyright (C) 2017 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;
18 
19 import android.annotation.NonNull;
20 import android.annotation.SuppressLint;
21 import android.app.NotificationChannel;
22 import android.app.NotificationManager;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.os.RemoteException;
26 import android.os.UserHandle;
27 import android.service.notification.StatusBarNotification;
28 import android.util.Log;
29 
30 import com.android.systemui.dagger.SysUISingleton;
31 import com.android.systemui.dagger.qualifiers.Main;
32 import com.android.systemui.plugins.PluginManager;
33 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
34 import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
35 import com.android.systemui.statusbar.notification.collection.NotifCollection;
36 import com.android.systemui.statusbar.notification.collection.PipelineDumpable;
37 import com.android.systemui.statusbar.notification.collection.PipelineDumper;
38 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
39 import com.android.systemui.statusbar.phone.CentralSurfaces;
40 import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins;
41 import com.android.systemui.util.time.SystemClock;
42 
43 import java.util.ArrayList;
44 import java.util.Deque;
45 import java.util.List;
46 import java.util.concurrent.ConcurrentLinkedDeque;
47 import java.util.concurrent.Executor;
48 
49 import javax.inject.Inject;
50 
51 /**
52  * This class handles listening to notification updates and passing them along to
53  * NotificationPresenter to be displayed to the user.
54  */
55 @SysUISingleton
56 @SuppressLint("OverrideAbstract")
57 public class NotificationListener extends NotificationListenerWithPlugins implements
58         PipelineDumpable {
59     private static final String TAG = "NotificationListener";
60     private static final boolean DEBUG = CentralSurfaces.DEBUG;
61     private static final long MAX_RANKING_DELAY_MILLIS = 500L;
62 
63     private final Context mContext;
64     private final NotificationManager mNotificationManager;
65     private final SilentNotificationStatusIconsVisibilityInteractor mStatusIconInteractor;
66     private final SystemClock mSystemClock;
67     private final Executor mMainExecutor;
68     private final List<NotificationHandler> mNotificationHandlers = new ArrayList<>();
69     private final ArrayList<NotificationSettingsListener> mSettingsListeners = new ArrayList<>();
70 
71     private final Deque<RankingMap> mRankingMapQueue = new ConcurrentLinkedDeque<>();
72     private final Runnable mDispatchRankingUpdateRunnable = this::dispatchRankingUpdate;
73     private long mSkippingRankingUpdatesSince = -1;
74 
75     /**
76      * Injected constructor. See {@link CentralSurfacesModule}.
77      */
78     @Inject
NotificationListener( Context context, NotificationManager notificationManager, SilentNotificationStatusIconsVisibilityInteractor statusIconInteractor, SystemClock systemClock, @Main Executor mainExecutor, PluginManager pluginManager)79     public NotificationListener(
80             Context context,
81             NotificationManager notificationManager,
82             SilentNotificationStatusIconsVisibilityInteractor statusIconInteractor,
83             SystemClock systemClock,
84             @Main Executor mainExecutor,
85             PluginManager pluginManager) {
86         super(pluginManager);
87         mContext = context;
88         mNotificationManager = notificationManager;
89         mStatusIconInteractor = statusIconInteractor;
90         mSystemClock = systemClock;
91         mMainExecutor = mainExecutor;
92     }
93 
94     /** Registers a listener that's notified when notifications are added/removed/etc. */
addNotificationHandler(NotificationHandler handler)95     public void addNotificationHandler(NotificationHandler handler) {
96         if (mNotificationHandlers.contains(handler)) {
97             throw new IllegalArgumentException("Listener is already added");
98         }
99         mNotificationHandlers.add(handler);
100     }
101 
102     /** Registers a listener that's notified when any notification-related settings change. */
103     @Deprecated
addNotificationSettingsListener(NotificationSettingsListener listener)104     public void addNotificationSettingsListener(NotificationSettingsListener listener) {
105         NotificationIconContainerRefactor.assertInLegacyMode();
106         mSettingsListeners.add(listener);
107     }
108 
109     @Override
onListenerConnected()110     public void onListenerConnected() {
111         if (DEBUG) Log.d(TAG, "onListenerConnected");
112         onPluginConnected();
113         final StatusBarNotification[] notifications = getActiveNotifications();
114         if (notifications == null) {
115             Log.w(TAG, "onListenerConnected unable to get active notifications.");
116             return;
117         }
118         final RankingMap currentRanking = getCurrentRanking();
119         mMainExecutor.execute(() -> {
120             // There's currently a race condition between the calls to getActiveNotifications() and
121             // getCurrentRanking(). It's possible for the ranking that we store here to not contain
122             // entries for every notification in getActiveNotifications(). To prevent downstream
123             // crashes, we temporarily fill in these missing rankings with stubs.
124             // See b/146011844 for long-term fix
125             final List<Ranking> newRankings = new ArrayList<>();
126             for (StatusBarNotification sbn : notifications) {
127                 newRankings.add(getRankingOrTemporaryStandIn(currentRanking, sbn.getKey()));
128             }
129             final RankingMap completeMap = new RankingMap(newRankings.toArray(new Ranking[0]));
130 
131             for (StatusBarNotification sbn : notifications) {
132                 for (NotificationHandler listener : mNotificationHandlers) {
133                     listener.onNotificationPosted(sbn, completeMap);
134                 }
135             }
136             for (NotificationHandler listener : mNotificationHandlers) {
137                 listener.onNotificationsInitialized();
138             }
139         });
140         onSilentStatusBarIconsVisibilityChanged(
141                 mNotificationManager.shouldHideSilentStatusBarIcons());
142     }
143 
144     @Override
onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap)145     public void onNotificationPosted(final StatusBarNotification sbn,
146             final RankingMap rankingMap) {
147         if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
148         if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
149             mMainExecutor.execute(() -> {
150                 for (NotificationHandler handler : mNotificationHandlers) {
151                     handler.onNotificationPosted(sbn, rankingMap);
152                 }
153             });
154         }
155     }
156 
157     @Override
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)158     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
159             int reason) {
160         if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn + " reason: " + reason);
161         if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
162             mMainExecutor.execute(() -> {
163                 for (NotificationHandler handler : mNotificationHandlers) {
164                     handler.onNotificationRemoved(sbn, rankingMap, reason);
165                 }
166             });
167         }
168     }
169 
170     @Override
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)171     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
172         onNotificationRemoved(sbn, rankingMap, NotifCollection.REASON_UNKNOWN);
173     }
174 
175     @Override
onNotificationRankingUpdate(final RankingMap rankingMap)176     public void onNotificationRankingUpdate(final RankingMap rankingMap) {
177         if (DEBUG) Log.d(TAG, "onRankingUpdate");
178         if (rankingMap != null) {
179             // Add the ranking to the queue, then run dispatchRankingUpdate() on the main thread
180             RankingMap r = onPluginRankingUpdate(rankingMap);
181             mRankingMapQueue.addLast(r);
182             // Maintaining our own queue and always posting the runnable allows us to guarantee the
183             //  relative ordering of all events which are dispatched, which is important so that the
184             //  RankingMap always has exactly the same elements that are current, per add/remove
185             //  events.
186             mMainExecutor.execute(mDispatchRankingUpdateRunnable);
187         }
188     }
189 
190     /**
191      * This method is (and must be) the sole consumer of the RankingMap queue.  After pulling an
192      * object off the queue, it checks if the queue is empty, and only dispatches the ranking update
193      * if the queue is still empty.
194      */
dispatchRankingUpdate()195     private void dispatchRankingUpdate() {
196         if (DEBUG) Log.d(TAG, "dispatchRankingUpdate");
197         RankingMap r = mRankingMapQueue.pollFirst();
198         if (r == null) {
199             Log.wtf(TAG, "mRankingMapQueue was empty!");
200         }
201         if (!mRankingMapQueue.isEmpty()) {
202             final long now = mSystemClock.elapsedRealtime();
203             if (mSkippingRankingUpdatesSince == -1) {
204                 mSkippingRankingUpdatesSince = now;
205             }
206             final long timeSkippingRankingUpdates = now - mSkippingRankingUpdatesSince;
207             if (timeSkippingRankingUpdates < MAX_RANKING_DELAY_MILLIS) {
208                 if (DEBUG) {
209                     Log.d(TAG, "Skipping dispatch of onNotificationRankingUpdate() -- "
210                             + mRankingMapQueue.size() + " more updates already in the queue.");
211                 }
212                 return;
213             }
214             if (DEBUG) {
215                 Log.d(TAG, "Proceeding with dispatch of onNotificationRankingUpdate() -- "
216                         + mRankingMapQueue.size() + " more updates already in the queue.");
217             }
218         }
219         mSkippingRankingUpdatesSince = -1;
220         for (NotificationHandler handler : mNotificationHandlers) {
221             handler.onNotificationRankingUpdate(r);
222         }
223     }
224 
225     @Override
onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)226     public void onNotificationChannelModified(
227             String pkgName, UserHandle user, NotificationChannel channel, int modificationType) {
228         if (DEBUG) Log.d(TAG, "onNotificationChannelModified");
229         if (!onPluginNotificationChannelModified(pkgName, user, channel, modificationType)) {
230             mMainExecutor.execute(() -> {
231                 for (NotificationHandler handler : mNotificationHandlers) {
232                     handler.onNotificationChannelModified(pkgName, user, channel, modificationType);
233                 }
234             });
235         }
236     }
237 
238     @Override
onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons)239     public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
240         if (NotificationIconContainerRefactor.isEnabled()) {
241             mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons);
242         } else {
243             for (NotificationSettingsListener listener : mSettingsListeners) {
244                 listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons);
245             }
246         }
247     }
248 
unsnoozeNotification(@onNull String key)249     public final void unsnoozeNotification(@NonNull String key) {
250         if (!isBound()) return;
251         try {
252             getNotificationInterface().unsnoozeNotificationFromSystemListener(mWrapper, key);
253         } catch (android.os.RemoteException ex) {
254             Log.v(TAG, "Unable to contact notification manager", ex);
255         }
256     }
257 
registerAsSystemService()258     public void registerAsSystemService() {
259         try {
260             registerAsSystemService(mContext,
261                     new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
262                     UserHandle.USER_ALL);
263         } catch (RemoteException e) {
264             Log.e(TAG, "Unable to register notification listener", e);
265         }
266     }
267 
268     @Override
dumpPipeline(@onNull PipelineDumper d)269     public void dumpPipeline(@NonNull PipelineDumper d) {
270         d.dump("notificationHandlers", mNotificationHandlers);
271     }
272 
getRankingOrTemporaryStandIn(RankingMap rankingMap, String key)273     private static Ranking getRankingOrTemporaryStandIn(RankingMap rankingMap, String key) {
274         Ranking ranking = new Ranking();
275         if (!rankingMap.getRanking(key, ranking)) {
276             ranking.populate(
277                     key,
278                     /* rank= */ 0,
279                     /* matchesInterruptionFilter= */ false,
280                     /* visibilityOverride= */ 0,
281                     /* suppressedVisualEffects= */ 0,
282                     /* importance= */ 0,
283                     /* explanation= */ null,
284                     /* overrideGroupKey= */ null,
285                     /* channel= */ null,
286                     /* overridePeople= */ new ArrayList<>(),
287                     /* snoozeCriteria= */ new ArrayList<>(),
288                     /* showBadge= */ false,
289                     /* userSentiment= */ 0,
290                     /* hidden= */ false,
291                     /* lastAudiblyAlertedMs= */ 0,
292                     /* noisy= */ false,
293                     /* smartActions= */ new ArrayList<>(),
294                     /* smartReplies= */ new ArrayList<>(),
295                     /* canBubble= */ false,
296                     /* isTextChanged= */ false,
297                     /* isConversation= */ false,
298                     /* shortcutInfo= */ null,
299                     /* rankingAdjustment= */ 0,
300                     /* isBubble= */ false,
301                     /* proposedImportance= */ 0,
302                     /* sensitiveContent= */ false
303             );
304         }
305         return ranking;
306     }
307 
308     @Deprecated
309     public interface NotificationSettingsListener {
310 
onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons)311         default void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { }
312     }
313 
314     /** Interface for listening to add/remove events that we receive from NotificationManager. */
315     public interface NotificationHandler {
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)316         void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap);
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)317         void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap);
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)318         void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason);
onNotificationRankingUpdate(RankingMap rankingMap)319         void onNotificationRankingUpdate(RankingMap rankingMap);
320 
321         /** Called after a notification channel is modified. */
onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)322         default void onNotificationChannelModified(
323                 String pkgName,
324                 UserHandle user,
325                 NotificationChannel channel,
326                 int modificationType) {
327         }
328 
329         /**
330          * Called after the listener has connected to NoMan and posted any current notifications.
331          */
onNotificationsInitialized()332         void onNotificationsInitialized();
333     }
334 }
335