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