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.server.IoThread;
42 
43 import java.io.File;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Set;
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         try {
88             NotificationHistoryJobService.scheduleJob(mContext);
89         } catch (Throwable e) {
90             Slog.e(TAG, "Failed to schedule cleanup job", e);
91         }
92         mSettingsObserver.observe();
93     }
94 
onUserUnlocked(@serIdInt int userId)95     void onUserUnlocked(@UserIdInt int userId) {
96         synchronized (mLock) {
97             mUserUnlockedStates.put(userId, true);
98             final NotificationHistoryDatabase userHistory =
99                     getUserHistoryAndInitializeIfNeededLocked(userId);
100             if (userHistory == null) {
101                 Slog.i(TAG, "Attempted to unlock gone/disabled user " + userId);
102                 return;
103             }
104 
105             // remove any packages that were deleted while the user was locked
106             final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
107             if (pendingPackageRemovals != null) {
108                 for (int i = 0; i < pendingPackageRemovals.size(); i++) {
109                     userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
110                 }
111                 mUserPendingPackageRemovals.remove(userId);
112             }
113 
114             // delete history if it was disabled when the user was locked
115             if (mUserPendingHistoryDisables.get(userId)) {
116                 disableHistory(userHistory, userId);
117             }
118         }
119     }
120 
onUserAdded(@serIdInt int userId)121     public void onUserAdded(@UserIdInt int userId) {
122         mSettingsObserver.update(null, userId);
123     }
124 
onUserStopped(@serIdInt int userId)125     public void onUserStopped(@UserIdInt int userId) {
126         synchronized (mLock) {
127             mUserUnlockedStates.put(userId, false);
128             mUserState.put(userId, null); // release the service (mainly for GC)
129         }
130     }
131 
onUserRemoved(@serIdInt int userId)132     public void onUserRemoved(@UserIdInt int userId) {
133         synchronized (mLock) {
134             // Actual data deletion is handled by other parts of the system (the entire directory is
135             // removed) - we just need clean up our internal state for GC
136             mUserPendingPackageRemovals.remove(userId);
137             mHistoryEnabled.put(userId, false);
138             mUserPendingHistoryDisables.put(userId, false);
139             onUserStopped(userId);
140         }
141     }
142 
onPackageRemoved(@serIdInt int userId, String packageName)143     public void onPackageRemoved(@UserIdInt int userId, String packageName) {
144         synchronized (mLock) {
145             if (!mUserUnlockedStates.get(userId, false)) {
146                 if (mHistoryEnabled.get(userId, false)) {
147                     List<String> userPendingRemovals =
148                             mUserPendingPackageRemovals.get(userId, new ArrayList<>());
149                     userPendingRemovals.add(packageName);
150                     mUserPendingPackageRemovals.put(userId, userPendingRemovals);
151                 }
152                 return;
153             }
154             final NotificationHistoryDatabase userHistory = mUserState.get(userId);
155             if (userHistory == null) {
156                 return;
157             }
158 
159             userHistory.onPackageRemoved(packageName);
160         }
161     }
162 
cleanupHistoryFiles()163     public void cleanupHistoryFiles() {
164         synchronized (mLock) {
165             int n = mUserUnlockedStates.size();
166             for (int i = 0;  i < n; i++) {
167                 // cleanup old files for currently unlocked users. User are additionally cleaned
168                 // on unlock in NotificationHistoryDatabase.init().
169                 if (mUserUnlockedStates.valueAt(i)) {
170                     final NotificationHistoryDatabase userHistory =
171                             mUserState.get(mUserUnlockedStates.keyAt(i));
172                     if (userHistory == null) {
173                         continue;
174                     }
175                     userHistory.prune();
176                 }
177             }
178         }
179     }
180 
deleteNotificationHistoryItem(String pkg, int uid, long postedTime)181     public void deleteNotificationHistoryItem(String pkg, int uid, long postedTime) {
182         synchronized (mLock) {
183             int userId = UserHandle.getUserId(uid);
184             final NotificationHistoryDatabase userHistory =
185                     getUserHistoryAndInitializeIfNeededLocked(userId);
186             // TODO: it shouldn't be possible to delete a notification entry while the user is
187             // locked but we should handle it
188             if (userHistory == null) {
189                 Slog.w(TAG, "Attempted to remove notif for locked/gone/disabled user "
190                         + userId);
191                 return;
192             }
193             userHistory.deleteNotificationHistoryItem(pkg, postedTime);
194         }
195     }
196 
deleteConversations(String pkg, int uid, Set<String> conversationIds)197     public void deleteConversations(String pkg, int uid, Set<String> conversationIds) {
198         synchronized (mLock) {
199             int userId = UserHandle.getUserId(uid);
200             final NotificationHistoryDatabase userHistory =
201                     getUserHistoryAndInitializeIfNeededLocked(userId);
202             // TODO: it shouldn't be possible to delete a notification entry while the user is
203             // locked but we should handle it
204             if (userHistory == null) {
205                 Slog.w(TAG, "Attempted to remove conversation for locked/gone/disabled user "
206                         + userId);
207                 return;
208             }
209             userHistory.deleteConversations(pkg, conversationIds);
210         }
211     }
212 
deleteNotificationChannel(String pkg, int uid, String channelId)213     public void deleteNotificationChannel(String pkg, int uid, String channelId) {
214         synchronized (mLock) {
215             int userId = UserHandle.getUserId(uid);
216             final NotificationHistoryDatabase userHistory =
217                     getUserHistoryAndInitializeIfNeededLocked(userId);
218             // TODO: it shouldn't be possible to delete a notification entry while the user is
219             // locked but we should handle it
220             if (userHistory == null) {
221                 Slog.w(TAG, "Attempted to remove channel for locked/gone/disabled user "
222                         + userId);
223                 return;
224             }
225             userHistory.deleteNotificationChannel(pkg, channelId);
226         }
227     }
228 
triggerWriteToDisk()229     public void triggerWriteToDisk() {
230         synchronized (mLock) {
231             final int userCount = mUserState.size();
232             for (int i = 0; i < userCount; i++) {
233                 final int userId = mUserState.keyAt(i);
234                 if (!mUserUnlockedStates.get(userId)) {
235                     continue;
236                 }
237                 NotificationHistoryDatabase userHistory = mUserState.get(userId);
238                 if (userHistory != null) {
239                     userHistory.forceWriteToDisk();
240                 }
241             }
242         }
243     }
244 
addNotification(@onNull final HistoricalNotification notification)245     public void addNotification(@NonNull final HistoricalNotification notification) {
246         Binder.withCleanCallingIdentity(() -> {
247             synchronized (mLock) {
248                 final NotificationHistoryDatabase userHistory =
249                         getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
250                 if (userHistory == null) {
251                     Slog.w(TAG, "Attempted to add notif for locked/gone/disabled user "
252                             + notification.getUserId());
253                     return;
254                 }
255                 userHistory.addNotification(notification);
256             }
257         });
258     }
259 
readNotificationHistory(@serIdInt int[] userIds)260     public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
261         synchronized (mLock) {
262             NotificationHistory mergedHistory = new NotificationHistory();
263             if (userIds == null) {
264                 return mergedHistory;
265             }
266             for (int userId : userIds) {
267                 final NotificationHistoryDatabase userHistory =
268                         getUserHistoryAndInitializeIfNeededLocked(userId);
269                 if (userHistory == null) {
270                     Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
271                     continue;
272                 }
273                 mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
274             }
275             return mergedHistory;
276         }
277     }
278 
readFilteredNotificationHistory( @serIdInt int userId, String packageName, String channelId, int maxNotifications)279     public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
280             @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
281         synchronized (mLock) {
282             final NotificationHistoryDatabase userHistory =
283                     getUserHistoryAndInitializeIfNeededLocked(userId);
284             if (userHistory == null) {
285                 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
286                 return new android.app.NotificationHistory();
287             }
288 
289             return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
290         }
291     }
292 
isHistoryEnabled(@serIdInt int userId)293     boolean isHistoryEnabled(@UserIdInt int userId) {
294         synchronized (mLock) {
295             return mHistoryEnabled.get(userId);
296         }
297     }
298 
onHistoryEnabledChanged(@serIdInt int userId, boolean historyEnabled)299     void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) {
300         synchronized (mLock) {
301             if (historyEnabled) {
302                 mHistoryEnabled.put(userId, historyEnabled);
303             }
304             final NotificationHistoryDatabase userHistory =
305                     getUserHistoryAndInitializeIfNeededLocked(userId);
306             if (userHistory != null) {
307                 if (!historyEnabled) {
308                     disableHistory(userHistory, userId);
309                 }
310             } else {
311                 mUserPendingHistoryDisables.put(userId, !historyEnabled);
312             }
313         }
314     }
315 
disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId)316     private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
317         userHistory.disableHistory();
318 
319         mUserPendingHistoryDisables.put(userId, false);
320         mHistoryEnabled.put(userId, false);
321         mUserState.put(userId, null);
322     }
323 
324     @GuardedBy("mLock")
getUserHistoryAndInitializeIfNeededLocked( int userId)325     private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
326             int userId) {
327         if (!mHistoryEnabled.get(userId)) {
328             if (DEBUG) {
329                 Slog.i(TAG, "History disabled for user " + userId);
330             }
331             mUserState.put(userId, null);
332             return null;
333         }
334         NotificationHistoryDatabase userHistory = mUserState.get(userId);
335         if (userHistory == null) {
336             final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
337                     DIRECTORY_PER_USER);
338             userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
339                     historyDir);
340             if (mUserUnlockedStates.get(userId)) {
341                 try {
342                     userHistory.init();
343                 } catch (Exception e) {
344                     if (mUserManager.isUserUnlocked(userId)) {
345                         throw e; // rethrow exception - user is unlocked
346                     } else {
347                         Slog.w(TAG, "Attempted to initialize service for "
348                                 + "stopped or removed user " + userId);
349                         return null;
350                     }
351                 }
352             } else {
353                 // locked! data unavailable
354                 Slog.w(TAG, "Attempted to initialize service for "
355                         + "stopped or removed user " + userId);
356                 return null;
357             }
358             mUserState.put(userId, userHistory);
359         }
360         return userHistory;
361     }
362 
363     @VisibleForTesting
isUserUnlocked(@serIdInt int userId)364     boolean isUserUnlocked(@UserIdInt int userId) {
365         synchronized (mLock) {
366             return mUserUnlockedStates.get(userId);
367         }
368     }
369 
370     @VisibleForTesting
doesHistoryExistForUser(@serIdInt int userId)371     boolean doesHistoryExistForUser(@UserIdInt int userId) {
372         synchronized (mLock) {
373             return mUserState.get(userId) != null;
374         }
375     }
376 
377     @VisibleForTesting
replaceNotificationHistoryDatabase(@serIdInt int userId, NotificationHistoryDatabase replacement)378     void replaceNotificationHistoryDatabase(@UserIdInt int userId,
379             NotificationHistoryDatabase replacement) {
380         synchronized (mLock) {
381             if (mUserState.get(userId) != null) {
382                 mUserState.put(userId, replacement);
383             }
384         }
385     }
386 
387     @VisibleForTesting
getPendingPackageRemovalsForUser(@serIdInt int userId)388     List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
389         synchronized (mLock) {
390             return mUserPendingPackageRemovals.get(userId);
391         }
392     }
393 
394     final class SettingsObserver extends ContentObserver {
395         private final Uri NOTIFICATION_HISTORY_URI
396                 = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
397 
SettingsObserver(Handler handler)398         SettingsObserver(Handler handler) {
399             super(handler);
400         }
401 
observe()402         void observe() {
403             ContentResolver resolver = mContext.getContentResolver();
404             resolver.registerContentObserver(NOTIFICATION_HISTORY_URI,
405                     false, this, UserHandle.USER_ALL);
406             synchronized (mLock) {
407                 for (UserInfo userInfo : mUserManager.getUsers()) {
408                     update(null, userInfo.id);
409                 }
410             }
411         }
412 
stopObserving()413         void stopObserving() {
414             ContentResolver resolver = mContext.getContentResolver();
415             resolver.unregisterContentObserver(this);
416         }
417 
418         @Override
onChange(boolean selfChange, Uri uri, int userId)419         public void onChange(boolean selfChange, Uri uri, int userId) {
420             update(uri, userId);
421         }
422 
update(Uri uri, int userId)423         public void update(Uri uri, int userId) {
424             ContentResolver resolver = mContext.getContentResolver();
425             if (uri == null || NOTIFICATION_HISTORY_URI.equals(uri)) {
426                 boolean historyEnabled = Settings.Secure.getIntForUser(resolver,
427                         Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId)
428                         != 0;
429                 onHistoryEnabledChanged(userId, historyEnabled);
430             }
431         }
432     }
433 }
434