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