blob: 69a7ce90f1c6efd345dce1577b173facd0524f7c [file] [log] [blame]
Julia Reynoldsa614c272019-11-15 16:43:29 -05001/*
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
17package com.android.server.notification;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.UserIdInt;
22import android.app.NotificationHistory;
23import android.app.NotificationHistory.HistoricalNotification;
Julia Reynoldse43fac32019-11-20 13:36:24 -050024import android.content.ContentResolver;
Julia Reynoldsa614c272019-11-15 16:43:29 -050025import android.content.Context;
Julia Reynoldse43fac32019-11-20 13:36:24 -050026import android.content.pm.UserInfo;
27import android.database.ContentObserver;
28import android.net.Uri;
Julia Reynolds037653b2020-04-23 12:53:35 -040029import android.os.Binder;
Julia Reynoldsa614c272019-11-15 16:43:29 -050030import android.os.Environment;
Julia Reynoldse43fac32019-11-20 13:36:24 -050031import android.os.Handler;
32import android.os.UserHandle;
Julia Reynoldsa614c272019-11-15 16:43:29 -050033import android.os.UserManager;
Julia Reynoldse43fac32019-11-20 13:36:24 -050034import android.provider.Settings;
Julia Reynoldsa614c272019-11-15 16:43:29 -050035import android.util.Slog;
36import android.util.SparseArray;
37import android.util.SparseBooleanArray;
38
39import com.android.internal.annotations.GuardedBy;
40import com.android.internal.annotations.VisibleForTesting;
Julia Reynolds037653b2020-04-23 12:53:35 -040041import com.android.internal.util.FunctionalUtils;
Julia Reynoldsa614c272019-11-15 16:43:29 -050042import com.android.server.IoThread;
Julia Reynoldsa614c272019-11-15 16:43:29 -050043
44import java.io.File;
45import java.util.ArrayList;
46import java.util.List;
47
48/**
49 * Keeps track of per-user notification histories.
50 */
51public 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;
Julia Reynoldse43fac32019-11-20 13:36:24 -050060 @VisibleForTesting
61 final SettingsObserver mSettingsObserver;
Julia Reynoldsa614c272019-11-15 16:43:29 -050062 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<>();
Julia Reynoldse43fac32019-11-20 13:36:24 -050070 @GuardedBy("mLock")
71 private final SparseBooleanArray mHistoryEnabled = new SparseBooleanArray();
Julia Reynolds6c111032020-01-17 12:32:59 -050072 @GuardedBy("mLock")
73 private final SparseBooleanArray mUserPendingHistoryDisables = new SparseBooleanArray();
Julia Reynoldsa614c272019-11-15 16:43:29 -050074
Julia Reynoldse43fac32019-11-20 13:36:24 -050075 public NotificationHistoryManager(Context context, Handler handler) {
Julia Reynoldsa614c272019-11-15 16:43:29 -050076 mContext = context;
77 mUserManager = context.getSystemService(UserManager.class);
Julia Reynoldse43fac32019-11-20 13:36:24 -050078 mSettingsObserver = new SettingsObserver(handler);
Julia Reynoldsa614c272019-11-15 16:43:29 -050079 }
80
Julia Reynolds6c111032020-01-17 12:32:59 -050081 @VisibleForTesting
82 void onDestroy() {
83 mSettingsObserver.stopObserving();
84 }
85
Julia Reynoldse43fac32019-11-20 13:36:24 -050086 void onBootPhaseAppsCanStart() {
87 mSettingsObserver.observe();
88 }
89
90 void onUserUnlocked(@UserIdInt int userId) {
Julia Reynoldsa614c272019-11-15 16:43:29 -050091 synchronized (mLock) {
92 mUserUnlockedStates.put(userId, true);
93 final NotificationHistoryDatabase userHistory =
94 getUserHistoryAndInitializeIfNeededLocked(userId);
95 if (userHistory == null) {
Julia Reynoldse43fac32019-11-20 13:36:24 -050096 Slog.i(TAG, "Attempted to unlock gone/disabled user " + userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -050097 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 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500108
109 // delete history if it was disabled when the user was locked
Julia Reynolds6c111032020-01-17 12:32:59 -0500110 if (mUserPendingHistoryDisables.get(userId)) {
111 disableHistory(userHistory, userId);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500112 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500113 }
114 }
115
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
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500123 public void onUserRemoved(@UserIdInt int userId) {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500124 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);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500128 mHistoryEnabled.put(userId, false);
Julia Reynolds6c111032020-01-17 12:32:59 -0500129 mUserPendingHistoryDisables.put(userId, false);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500130 onUserStopped(userId);
131 }
132 }
133
Julia Reynoldsb1a77182020-02-06 16:38:10 -0500134 public void onPackageRemoved(@UserIdInt int userId, String packageName) {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500135 synchronized (mLock) {
136 if (!mUserUnlockedStates.get(userId, false)) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500137 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 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500143 return;
144 }
145 final NotificationHistoryDatabase userHistory = mUserState.get(userId);
146 if (userHistory == null) {
147 return;
148 }
149
150 userHistory.onPackageRemoved(packageName);
151 }
152 }
153
Julia Reynoldsb1a77182020-02-06 16:38:10 -0500154 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
Julia Reynoldsa625b942020-02-15 09:42:23 -0500170 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
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500186 public void triggerWriteToDisk() {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500187 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
202 public void addNotification(@NonNull final HistoricalNotification notification) {
Julia Reynolds037653b2020-04-23 12:53:35 -0400203 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);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500213 }
Julia Reynolds037653b2020-04-23 12:53:35 -0400214 });
Julia Reynoldsa614c272019-11-15 16:43:29 -0500215 }
216
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) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500227 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500228 continue;
229 }
230 mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
231 }
232 return mergedHistory;
233 }
234 }
235
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) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500242 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500243 return new android.app.NotificationHistory();
244 }
245
246 return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
247 }
248 }
249
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500250 boolean isHistoryEnabled(@UserIdInt int userId) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500251 synchronized (mLock) {
252 return mHistoryEnabled.get(userId);
253 }
254 }
255
256 void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) {
257 synchronized (mLock) {
Julia Reynolds6c111032020-01-17 12:32:59 -0500258 if (historyEnabled) {
259 mHistoryEnabled.put(userId, historyEnabled);
260 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500261 final NotificationHistoryDatabase userHistory =
262 getUserHistoryAndInitializeIfNeededLocked(userId);
263 if (userHistory != null) {
264 if (!historyEnabled) {
Julia Reynolds6c111032020-01-17 12:32:59 -0500265 disableHistory(userHistory, userId);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500266 }
Julia Reynolds6c111032020-01-17 12:32:59 -0500267 } else {
268 mUserPendingHistoryDisables.put(userId, !historyEnabled);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500269 }
270 }
271 }
272
Julia Reynolds6c111032020-01-17 12:32:59 -0500273 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
Julia Reynoldsa614c272019-11-15 16:43:29 -0500281 @GuardedBy("mLock")
282 private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
283 int userId) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500284 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 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500291 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(),
Julia Reynolds575e0882020-02-28 17:02:11 -0500296 historyDir);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500297 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
321 boolean isUserUnlocked(@UserIdInt int userId) {
322 synchronized (mLock) {
323 return mUserUnlockedStates.get(userId);
324 }
325 }
326
327 @VisibleForTesting
328 boolean doesHistoryExistForUser(@UserIdInt int userId) {
329 synchronized (mLock) {
330 return mUserState.get(userId) != null;
331 }
332 }
333
334 @VisibleForTesting
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
345 List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
346 synchronized (mLock) {
347 return mUserPendingPackageRemovals.get(userId);
348 }
349 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500350
351 final class SettingsObserver extends ContentObserver {
352 private final Uri NOTIFICATION_HISTORY_URI
353 = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
354
355 SettingsObserver(Handler handler) {
356 super(handler);
357 }
358
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()) {
Julia Reynoldsfd9f8342020-03-06 09:36:00 -0500365 if (!userInfo.isProfile()) {
366 update(null, userInfo.id);
367 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500368 }
369 }
370 }
371
Julia Reynolds6c111032020-01-17 12:32:59 -0500372 void stopObserving() {
373 ContentResolver resolver = mContext.getContentResolver();
374 resolver.unregisterContentObserver(this);
375 }
376
Julia Reynoldse43fac32019-11-20 13:36:24 -0500377 @Override
378 public void onChange(boolean selfChange, Uri uri, int userId) {
379 update(uri, userId);
380 }
381
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;
Julia Reynoldsfd9f8342020-03-06 09:36:00 -0500388 int[] profiles = mUserManager.getProfileIds(userId, true);
389 for (int profileId : profiles) {
390 onHistoryEnabledChanged(profileId, historyEnabled);
391 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500392 }
393 }
394 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500395}