• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.server.notification;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.NotificationHistory;
23 import android.app.NotificationHistory.HistoricalNotification;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.UserInfo;
27 import android.database.ContentObserver;
28 import android.net.Uri;
29 import android.os.Binder;
30 import android.os.Environment;
31 import android.os.Handler;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.provider.Settings;
35 import android.util.Slog;
36 import android.util.SparseArray;
37 import android.util.SparseBooleanArray;
38 
39 import com.android.internal.annotations.GuardedBy;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.util.FunctionalUtils;
42 import com.android.server.IoThread;
43 
44 import java.io.File;
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 /**
49  * Keeps track of per-user notification histories.
50  */
51 public class NotificationHistoryManager {
52     private static final String TAG = "NotificationHistory";
53     private static final boolean DEBUG = NotificationManagerService.DBG;
54 
55     @VisibleForTesting
56     static final String DIRECTORY_PER_USER = "notification_history";
57 
58     private final Context mContext;
59     private final UserManager mUserManager;
60     @VisibleForTesting
61     final SettingsObserver mSettingsObserver;
62     private final Object mLock = new Object();
63     @GuardedBy("mLock")
64     private final SparseArray<NotificationHistoryDatabase> mUserState = new SparseArray<>();
65     @GuardedBy("mLock")
66     private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
67     // TODO: does this need to be persisted across reboots?
68     @GuardedBy("mLock")
69     private final SparseArray<List<String>> mUserPendingPackageRemovals = new SparseArray<>();
70     @GuardedBy("mLock")
71     private final SparseBooleanArray mHistoryEnabled = new SparseBooleanArray();
72     @GuardedBy("mLock")
73     private final SparseBooleanArray mUserPendingHistoryDisables = new SparseBooleanArray();
74 
NotificationHistoryManager(Context context, Handler handler)75     public NotificationHistoryManager(Context context, Handler handler) {
76         mContext = context;
77         mUserManager = context.getSystemService(UserManager.class);
78         mSettingsObserver = new SettingsObserver(handler);
79     }
80 
81     @VisibleForTesting
onDestroy()82     void onDestroy() {
83         mSettingsObserver.stopObserving();
84     }
85 
onBootPhaseAppsCanStart()86     void onBootPhaseAppsCanStart() {
87         mSettingsObserver.observe();
88     }
89 
onUserUnlocked(@serIdInt int userId)90     void onUserUnlocked(@UserIdInt int userId) {
91         synchronized (mLock) {
92             mUserUnlockedStates.put(userId, true);
93             final NotificationHistoryDatabase userHistory =
94                     getUserHistoryAndInitializeIfNeededLocked(userId);
95             if (userHistory == null) {
96                 Slog.i(TAG, "Attempted to unlock gone/disabled user " + userId);
97                 return;
98             }
99 
100             // remove any packages that were deleted while the user was locked
101             final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
102             if (pendingPackageRemovals != null) {
103                 for (int i = 0; i < pendingPackageRemovals.size(); i++) {
104                     userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
105                 }
106                 mUserPendingPackageRemovals.put(userId, null);
107             }
108 
109             // delete history if it was disabled when the user was locked
110             if (mUserPendingHistoryDisables.get(userId)) {
111                 disableHistory(userHistory, userId);
112             }
113         }
114     }
115 
onUserStopped(@serIdInt int userId)116     public void onUserStopped(@UserIdInt int userId) {
117         synchronized (mLock) {
118             mUserUnlockedStates.put(userId, false);
119             mUserState.put(userId, null); // release the service (mainly for GC)
120         }
121     }
122 
onUserRemoved(@serIdInt int userId)123     public void onUserRemoved(@UserIdInt int userId) {
124         synchronized (mLock) {
125             // Actual data deletion is handled by other parts of the system (the entire directory is
126             // removed) - we just need clean up our internal state for GC
127             mUserPendingPackageRemovals.put(userId, null);
128             mHistoryEnabled.put(userId, false);
129             mUserPendingHistoryDisables.put(userId, false);
130             onUserStopped(userId);
131         }
132     }
133 
onPackageRemoved(@serIdInt int userId, String packageName)134     public void onPackageRemoved(@UserIdInt int userId, String packageName) {
135         synchronized (mLock) {
136             if (!mUserUnlockedStates.get(userId, false)) {
137                 if (mHistoryEnabled.get(userId, false)) {
138                     List<String> userPendingRemovals =
139                             mUserPendingPackageRemovals.get(userId, new ArrayList<>());
140                     userPendingRemovals.add(packageName);
141                     mUserPendingPackageRemovals.put(userId, userPendingRemovals);
142                 }
143                 return;
144             }
145             final NotificationHistoryDatabase userHistory = mUserState.get(userId);
146             if (userHistory == null) {
147                 return;
148             }
149 
150             userHistory.onPackageRemoved(packageName);
151         }
152     }
153 
deleteNotificationHistoryItem(String pkg, int uid, long postedTime)154     public void deleteNotificationHistoryItem(String pkg, int uid, long postedTime) {
155         synchronized (mLock) {
156             int userId = UserHandle.getUserId(uid);
157             final NotificationHistoryDatabase userHistory =
158                     getUserHistoryAndInitializeIfNeededLocked(userId);
159             // TODO: it shouldn't be possible to delete a notification entry while the user is
160             // locked but we should handle it
161             if (userHistory == null) {
162                 Slog.w(TAG, "Attempted to remove notif for locked/gone/disabled user "
163                         + userId);
164                 return;
165             }
166             userHistory.deleteNotificationHistoryItem(pkg, postedTime);
167         }
168     }
169 
deleteConversation(String pkg, int uid, String conversationId)170     public void deleteConversation(String pkg, int uid, String conversationId) {
171         synchronized (mLock) {
172             int userId = UserHandle.getUserId(uid);
173             final NotificationHistoryDatabase userHistory =
174                     getUserHistoryAndInitializeIfNeededLocked(userId);
175             // TODO: it shouldn't be possible to delete a notification entry while the user is
176             // locked but we should handle it
177             if (userHistory == null) {
178                 Slog.w(TAG, "Attempted to remove conversation for locked/gone/disabled user "
179                         + userId);
180                 return;
181             }
182             userHistory.deleteConversation(pkg, conversationId);
183         }
184     }
185 
triggerWriteToDisk()186     public void triggerWriteToDisk() {
187         synchronized (mLock) {
188             final int userCount = mUserState.size();
189             for (int i = 0; i < userCount; i++) {
190                 final int userId = mUserState.keyAt(i);
191                 if (!mUserUnlockedStates.get(userId)) {
192                     continue;
193                 }
194                 NotificationHistoryDatabase userHistory = mUserState.get(userId);
195                 if (userHistory != null) {
196                     userHistory.forceWriteToDisk();
197                 }
198             }
199         }
200     }
201 
addNotification(@onNull final HistoricalNotification notification)202     public void addNotification(@NonNull final HistoricalNotification notification) {
203         Binder.withCleanCallingIdentity(() -> {
204             synchronized (mLock) {
205                 final NotificationHistoryDatabase userHistory =
206                         getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
207                 if (userHistory == null) {
208                     Slog.w(TAG, "Attempted to add notif for locked/gone/disabled user "
209                             + notification.getUserId());
210                     return;
211                 }
212                 userHistory.addNotification(notification);
213             }
214         });
215     }
216 
readNotificationHistory(@serIdInt int[] userIds)217     public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
218         synchronized (mLock) {
219             NotificationHistory mergedHistory = new NotificationHistory();
220             if (userIds == null) {
221                 return mergedHistory;
222             }
223             for (int userId : userIds) {
224                 final NotificationHistoryDatabase userHistory =
225                         getUserHistoryAndInitializeIfNeededLocked(userId);
226                 if (userHistory == null) {
227                     Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
228                     continue;
229                 }
230                 mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
231             }
232             return mergedHistory;
233         }
234     }
235 
readFilteredNotificationHistory( @serIdInt int userId, String packageName, String channelId, int maxNotifications)236     public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
237             @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
238         synchronized (mLock) {
239             final NotificationHistoryDatabase userHistory =
240                     getUserHistoryAndInitializeIfNeededLocked(userId);
241             if (userHistory == null) {
242                 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
243                 return new android.app.NotificationHistory();
244             }
245 
246             return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
247         }
248     }
249 
isHistoryEnabled(@serIdInt int userId)250     boolean isHistoryEnabled(@UserIdInt int userId) {
251         synchronized (mLock) {
252             return mHistoryEnabled.get(userId);
253         }
254     }
255 
onHistoryEnabledChanged(@serIdInt int userId, boolean historyEnabled)256     void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) {
257         synchronized (mLock) {
258             if (historyEnabled) {
259                 mHistoryEnabled.put(userId, historyEnabled);
260             }
261             final NotificationHistoryDatabase userHistory =
262                     getUserHistoryAndInitializeIfNeededLocked(userId);
263             if (userHistory != null) {
264                 if (!historyEnabled) {
265                     disableHistory(userHistory, userId);
266                 }
267             } else {
268                 mUserPendingHistoryDisables.put(userId, !historyEnabled);
269             }
270         }
271     }
272 
disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId)273     private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
274         userHistory.disableHistory();
275 
276         mUserPendingHistoryDisables.put(userId, false);
277         mHistoryEnabled.put(userId, false);
278         mUserState.put(userId, null);
279     }
280 
281     @GuardedBy("mLock")
getUserHistoryAndInitializeIfNeededLocked( int userId)282     private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
283             int userId) {
284         if (!mHistoryEnabled.get(userId)) {
285             if (DEBUG) {
286                 Slog.i(TAG, "History disabled for user " + userId);
287             }
288             mUserState.put(userId, null);
289             return null;
290         }
291         NotificationHistoryDatabase userHistory = mUserState.get(userId);
292         if (userHistory == null) {
293             final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
294                     DIRECTORY_PER_USER);
295             userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
296                     historyDir);
297             if (mUserUnlockedStates.get(userId)) {
298                 try {
299                     userHistory.init();
300                 } catch (Exception e) {
301                     if (mUserManager.isUserUnlocked(userId)) {
302                         throw e; // rethrow exception - user is unlocked
303                     } else {
304                         Slog.w(TAG, "Attempted to initialize service for "
305                                 + "stopped or removed user " + userId);
306                         return null;
307                     }
308                 }
309             } else {
310                 // locked! data unavailable
311                 Slog.w(TAG, "Attempted to initialize service for "
312                         + "stopped or removed user " + userId);
313                 return null;
314             }
315             mUserState.put(userId, userHistory);
316         }
317         return userHistory;
318     }
319 
320     @VisibleForTesting
isUserUnlocked(@serIdInt int userId)321     boolean isUserUnlocked(@UserIdInt int userId) {
322         synchronized (mLock) {
323             return mUserUnlockedStates.get(userId);
324         }
325     }
326 
327     @VisibleForTesting
doesHistoryExistForUser(@serIdInt int userId)328     boolean doesHistoryExistForUser(@UserIdInt int userId) {
329         synchronized (mLock) {
330             return mUserState.get(userId) != null;
331         }
332     }
333 
334     @VisibleForTesting
replaceNotificationHistoryDatabase(@serIdInt int userId, NotificationHistoryDatabase replacement)335     void replaceNotificationHistoryDatabase(@UserIdInt int userId,
336             NotificationHistoryDatabase replacement) {
337         synchronized (mLock) {
338             if (mUserState.get(userId) != null) {
339                 mUserState.put(userId, replacement);
340             }
341         }
342     }
343 
344     @VisibleForTesting
getPendingPackageRemovalsForUser(@serIdInt int userId)345     List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
346         synchronized (mLock) {
347             return mUserPendingPackageRemovals.get(userId);
348         }
349     }
350 
351     final class SettingsObserver extends ContentObserver {
352         private final Uri NOTIFICATION_HISTORY_URI
353                 = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
354 
SettingsObserver(Handler handler)355         SettingsObserver(Handler handler) {
356             super(handler);
357         }
358 
observe()359         void observe() {
360             ContentResolver resolver = mContext.getContentResolver();
361             resolver.registerContentObserver(NOTIFICATION_HISTORY_URI,
362                     false, this, UserHandle.USER_ALL);
363             synchronized (mLock) {
364                 for (UserInfo userInfo : mUserManager.getUsers()) {
365                     if (!userInfo.isProfile()) {
366                         update(null, userInfo.id);
367                     }
368                 }
369             }
370         }
371 
stopObserving()372         void stopObserving() {
373             ContentResolver resolver = mContext.getContentResolver();
374             resolver.unregisterContentObserver(this);
375         }
376 
377         @Override
onChange(boolean selfChange, Uri uri, int userId)378         public void onChange(boolean selfChange, Uri uri, int userId) {
379             update(uri, userId);
380         }
381 
update(Uri uri, int userId)382         public void update(Uri uri, int userId) {
383             ContentResolver resolver = mContext.getContentResolver();
384             if (uri == null || NOTIFICATION_HISTORY_URI.equals(uri)) {
385                 boolean historyEnabled = Settings.Secure.getIntForUser(resolver,
386                         Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId)
387                         != 0;
388                 int[] profiles = mUserManager.getProfileIds(userId, true);
389                 for (int profileId : profiles) {
390                     onHistoryEnabledChanged(profileId, historyEnabled);
391                 }
392             }
393         }
394     }
395 }
396