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         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 
deleteConversations(String pkg, int uid, Set<String> conversationIds)170     public void deleteConversations(String pkg, int uid, Set<String> conversationIds) {
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.deleteConversations(pkg, conversationIds);
183         }
184     }
185 
deleteNotificationChannel(String pkg, int uid, String channelId)186     public void deleteNotificationChannel(String pkg, int uid, String channelId) {
187         synchronized (mLock) {
188             int userId = UserHandle.getUserId(uid);
189             final NotificationHistoryDatabase userHistory =
190                     getUserHistoryAndInitializeIfNeededLocked(userId);
191             // TODO: it shouldn't be possible to delete a notification entry while the user is
192             // locked but we should handle it
193             if (userHistory == null) {
194                 Slog.w(TAG, "Attempted to remove channel for locked/gone/disabled user "
195                         + userId);
196                 return;
197             }
198             userHistory.deleteNotificationChannel(pkg, channelId);
199         }
200     }
201 
triggerWriteToDisk()202     public void triggerWriteToDisk() {
203         synchronized (mLock) {
204             final int userCount = mUserState.size();
205             for (int i = 0; i < userCount; i++) {
206                 final int userId = mUserState.keyAt(i);
207                 if (!mUserUnlockedStates.get(userId)) {
208                     continue;
209                 }
210                 NotificationHistoryDatabase userHistory = mUserState.get(userId);
211                 if (userHistory != null) {
212                     userHistory.forceWriteToDisk();
213                 }
214             }
215         }
216     }
217 
addNotification(@onNull final HistoricalNotification notification)218     public void addNotification(@NonNull final HistoricalNotification notification) {
219         Binder.withCleanCallingIdentity(() -> {
220             synchronized (mLock) {
221                 final NotificationHistoryDatabase userHistory =
222                         getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
223                 if (userHistory == null) {
224                     Slog.w(TAG, "Attempted to add notif for locked/gone/disabled user "
225                             + notification.getUserId());
226                     return;
227                 }
228                 userHistory.addNotification(notification);
229             }
230         });
231     }
232 
readNotificationHistory(@serIdInt int[] userIds)233     public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
234         synchronized (mLock) {
235             NotificationHistory mergedHistory = new NotificationHistory();
236             if (userIds == null) {
237                 return mergedHistory;
238             }
239             for (int userId : userIds) {
240                 final NotificationHistoryDatabase userHistory =
241                         getUserHistoryAndInitializeIfNeededLocked(userId);
242                 if (userHistory == null) {
243                     Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
244                     continue;
245                 }
246                 mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
247             }
248             return mergedHistory;
249         }
250     }
251 
readFilteredNotificationHistory( @serIdInt int userId, String packageName, String channelId, int maxNotifications)252     public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
253             @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
254         synchronized (mLock) {
255             final NotificationHistoryDatabase userHistory =
256                     getUserHistoryAndInitializeIfNeededLocked(userId);
257             if (userHistory == null) {
258                 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
259                 return new android.app.NotificationHistory();
260             }
261 
262             return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
263         }
264     }
265 
isHistoryEnabled(@serIdInt int userId)266     boolean isHistoryEnabled(@UserIdInt int userId) {
267         synchronized (mLock) {
268             return mHistoryEnabled.get(userId);
269         }
270     }
271 
onHistoryEnabledChanged(@serIdInt int userId, boolean historyEnabled)272     void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) {
273         synchronized (mLock) {
274             if (historyEnabled) {
275                 mHistoryEnabled.put(userId, historyEnabled);
276             }
277             final NotificationHistoryDatabase userHistory =
278                     getUserHistoryAndInitializeIfNeededLocked(userId);
279             if (userHistory != null) {
280                 if (!historyEnabled) {
281                     disableHistory(userHistory, userId);
282                 }
283             } else {
284                 mUserPendingHistoryDisables.put(userId, !historyEnabled);
285             }
286         }
287     }
288 
disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId)289     private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
290         userHistory.disableHistory();
291 
292         mUserPendingHistoryDisables.put(userId, false);
293         mHistoryEnabled.put(userId, false);
294         mUserState.put(userId, null);
295     }
296 
297     @GuardedBy("mLock")
getUserHistoryAndInitializeIfNeededLocked( int userId)298     private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
299             int userId) {
300         if (!mHistoryEnabled.get(userId)) {
301             if (DEBUG) {
302                 Slog.i(TAG, "History disabled for user " + userId);
303             }
304             mUserState.put(userId, null);
305             return null;
306         }
307         NotificationHistoryDatabase userHistory = mUserState.get(userId);
308         if (userHistory == null) {
309             final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
310                     DIRECTORY_PER_USER);
311             userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
312                     historyDir);
313             if (mUserUnlockedStates.get(userId)) {
314                 try {
315                     userHistory.init();
316                 } catch (Exception e) {
317                     if (mUserManager.isUserUnlocked(userId)) {
318                         throw e; // rethrow exception - user is unlocked
319                     } else {
320                         Slog.w(TAG, "Attempted to initialize service for "
321                                 + "stopped or removed user " + userId);
322                         return null;
323                     }
324                 }
325             } else {
326                 // locked! data unavailable
327                 Slog.w(TAG, "Attempted to initialize service for "
328                         + "stopped or removed user " + userId);
329                 return null;
330             }
331             mUserState.put(userId, userHistory);
332         }
333         return userHistory;
334     }
335 
336     @VisibleForTesting
isUserUnlocked(@serIdInt int userId)337     boolean isUserUnlocked(@UserIdInt int userId) {
338         synchronized (mLock) {
339             return mUserUnlockedStates.get(userId);
340         }
341     }
342 
343     @VisibleForTesting
doesHistoryExistForUser(@serIdInt int userId)344     boolean doesHistoryExistForUser(@UserIdInt int userId) {
345         synchronized (mLock) {
346             return mUserState.get(userId) != null;
347         }
348     }
349 
350     @VisibleForTesting
replaceNotificationHistoryDatabase(@serIdInt int userId, NotificationHistoryDatabase replacement)351     void replaceNotificationHistoryDatabase(@UserIdInt int userId,
352             NotificationHistoryDatabase replacement) {
353         synchronized (mLock) {
354             if (mUserState.get(userId) != null) {
355                 mUserState.put(userId, replacement);
356             }
357         }
358     }
359 
360     @VisibleForTesting
getPendingPackageRemovalsForUser(@serIdInt int userId)361     List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
362         synchronized (mLock) {
363             return mUserPendingPackageRemovals.get(userId);
364         }
365     }
366 
367     final class SettingsObserver extends ContentObserver {
368         private final Uri NOTIFICATION_HISTORY_URI
369                 = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
370 
SettingsObserver(Handler handler)371         SettingsObserver(Handler handler) {
372             super(handler);
373         }
374 
observe()375         void observe() {
376             ContentResolver resolver = mContext.getContentResolver();
377             resolver.registerContentObserver(NOTIFICATION_HISTORY_URI,
378                     false, this, UserHandle.USER_ALL);
379             synchronized (mLock) {
380                 for (UserInfo userInfo : mUserManager.getUsers()) {
381                     if (!userInfo.isProfile()) {
382                         update(null, userInfo.id);
383                     }
384                 }
385             }
386         }
387 
stopObserving()388         void stopObserving() {
389             ContentResolver resolver = mContext.getContentResolver();
390             resolver.unregisterContentObserver(this);
391         }
392 
393         @Override
onChange(boolean selfChange, Uri uri, int userId)394         public void onChange(boolean selfChange, Uri uri, int userId) {
395             update(uri, userId);
396         }
397 
update(Uri uri, int userId)398         public void update(Uri uri, int userId) {
399             ContentResolver resolver = mContext.getContentResolver();
400             if (uri == null || NOTIFICATION_HISTORY_URI.equals(uri)) {
401                 boolean historyEnabled = Settings.Secure.getIntForUser(resolver,
402                         Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId)
403                         != 0;
404                 int[] profiles = mUserManager.getProfileIds(userId, true);
405                 for (int profileId : profiles) {
406                     onHistoryEnabledChanged(profileId, historyEnabled);
407                 }
408             }
409         }
410     }
411 }
412