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