blob: 41bc29f7a82e5c98b69121efe98b8824fc6a93c0 [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 Reynoldsa614c272019-11-15 16:43:29 -050029import android.os.Environment;
Julia Reynoldse43fac32019-11-20 13:36:24 -050030import android.os.Handler;
31import android.os.UserHandle;
Julia Reynoldsa614c272019-11-15 16:43:29 -050032import android.os.UserManager;
Julia Reynoldse43fac32019-11-20 13:36:24 -050033import android.provider.Settings;
Julia Reynoldsa614c272019-11-15 16:43:29 -050034import android.util.Slog;
35import android.util.SparseArray;
36import android.util.SparseBooleanArray;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.internal.annotations.VisibleForTesting;
40import com.android.server.IoThread;
41import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider;
42
43import java.io.File;
44import java.util.ArrayList;
45import java.util.List;
46
47/**
48 * Keeps track of per-user notification histories.
49 */
50public class NotificationHistoryManager {
51 private static final String TAG = "NotificationHistory";
52 private static final boolean DEBUG = NotificationManagerService.DBG;
53
54 @VisibleForTesting
55 static final String DIRECTORY_PER_USER = "notification_history";
56
57 private final Context mContext;
58 private final UserManager mUserManager;
Julia Reynoldse43fac32019-11-20 13:36:24 -050059 @VisibleForTesting
60 final SettingsObserver mSettingsObserver;
Julia Reynoldsa614c272019-11-15 16:43:29 -050061 private final Object mLock = new Object();
62 @GuardedBy("mLock")
63 private final SparseArray<NotificationHistoryDatabase> mUserState = new SparseArray<>();
64 @GuardedBy("mLock")
65 private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
66 // TODO: does this need to be persisted across reboots?
67 @GuardedBy("mLock")
68 private final SparseArray<List<String>> mUserPendingPackageRemovals = new SparseArray<>();
Julia Reynoldse43fac32019-11-20 13:36:24 -050069 @GuardedBy("mLock")
70 private final SparseBooleanArray mHistoryEnabled = new SparseBooleanArray();
Julia Reynolds6c111032020-01-17 12:32:59 -050071 @GuardedBy("mLock")
72 private final SparseBooleanArray mUserPendingHistoryDisables = new SparseBooleanArray();
Julia Reynoldsa614c272019-11-15 16:43:29 -050073
Julia Reynoldse43fac32019-11-20 13:36:24 -050074 public NotificationHistoryManager(Context context, Handler handler) {
Julia Reynoldsa614c272019-11-15 16:43:29 -050075 mContext = context;
76 mUserManager = context.getSystemService(UserManager.class);
Julia Reynoldse43fac32019-11-20 13:36:24 -050077 mSettingsObserver = new SettingsObserver(handler);
Julia Reynoldsa614c272019-11-15 16:43:29 -050078 }
79
Julia Reynolds6c111032020-01-17 12:32:59 -050080 @VisibleForTesting
81 void onDestroy() {
82 mSettingsObserver.stopObserving();
83 }
84
Julia Reynoldse43fac32019-11-20 13:36:24 -050085 void onBootPhaseAppsCanStart() {
86 mSettingsObserver.observe();
87 }
88
89 void onUserUnlocked(@UserIdInt int userId) {
Julia Reynoldsa614c272019-11-15 16:43:29 -050090 synchronized (mLock) {
91 mUserUnlockedStates.put(userId, true);
92 final NotificationHistoryDatabase userHistory =
93 getUserHistoryAndInitializeIfNeededLocked(userId);
94 if (userHistory == null) {
Julia Reynoldse43fac32019-11-20 13:36:24 -050095 Slog.i(TAG, "Attempted to unlock gone/disabled user " + userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -050096 return;
97 }
98
99 // remove any packages that were deleted while the user was locked
100 final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
101 if (pendingPackageRemovals != null) {
102 for (int i = 0; i < pendingPackageRemovals.size(); i++) {
103 userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
104 }
105 mUserPendingPackageRemovals.put(userId, null);
106 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500107
108 // delete history if it was disabled when the user was locked
Julia Reynolds6c111032020-01-17 12:32:59 -0500109 if (mUserPendingHistoryDisables.get(userId)) {
110 disableHistory(userHistory, userId);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500111 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500112 }
113 }
114
115 public void onUserStopped(@UserIdInt int userId) {
116 synchronized (mLock) {
117 mUserUnlockedStates.put(userId, false);
118 mUserState.put(userId, null); // release the service (mainly for GC)
119 }
120 }
121
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500122 public void onUserRemoved(@UserIdInt int userId) {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500123 synchronized (mLock) {
124 // Actual data deletion is handled by other parts of the system (the entire directory is
125 // removed) - we just need clean up our internal state for GC
126 mUserPendingPackageRemovals.put(userId, null);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500127 mHistoryEnabled.put(userId, false);
Julia Reynolds6c111032020-01-17 12:32:59 -0500128 mUserPendingHistoryDisables.put(userId, false);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500129 onUserStopped(userId);
130 }
131 }
132
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500133 public void onPackageRemoved(int userId, String packageName) {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500134 synchronized (mLock) {
135 if (!mUserUnlockedStates.get(userId, false)) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500136 if (mHistoryEnabled.get(userId, false)) {
137 List<String> userPendingRemovals =
138 mUserPendingPackageRemovals.get(userId, new ArrayList<>());
139 userPendingRemovals.add(packageName);
140 mUserPendingPackageRemovals.put(userId, userPendingRemovals);
141 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500142 return;
143 }
144 final NotificationHistoryDatabase userHistory = mUserState.get(userId);
145 if (userHistory == null) {
146 return;
147 }
148
149 userHistory.onPackageRemoved(packageName);
150 }
151 }
152
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500153 // TODO: wire this up to AMS when power button is long pressed
154 public void triggerWriteToDisk() {
Julia Reynoldsa614c272019-11-15 16:43:29 -0500155 synchronized (mLock) {
156 final int userCount = mUserState.size();
157 for (int i = 0; i < userCount; i++) {
158 final int userId = mUserState.keyAt(i);
159 if (!mUserUnlockedStates.get(userId)) {
160 continue;
161 }
162 NotificationHistoryDatabase userHistory = mUserState.get(userId);
163 if (userHistory != null) {
164 userHistory.forceWriteToDisk();
165 }
166 }
167 }
168 }
169
170 public void addNotification(@NonNull final HistoricalNotification notification) {
171 synchronized (mLock) {
172 final NotificationHistoryDatabase userHistory =
173 getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
174 if (userHistory == null) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500175 Slog.w(TAG, "Attempted to add notif for locked/gone/disabled user "
Julia Reynoldsa614c272019-11-15 16:43:29 -0500176 + notification.getUserId());
177 return;
178 }
179 userHistory.addNotification(notification);
180 }
181 }
182
183 public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
184 synchronized (mLock) {
185 NotificationHistory mergedHistory = new NotificationHistory();
186 if (userIds == null) {
187 return mergedHistory;
188 }
189 for (int userId : userIds) {
190 final NotificationHistoryDatabase userHistory =
191 getUserHistoryAndInitializeIfNeededLocked(userId);
192 if (userHistory == null) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500193 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500194 continue;
195 }
196 mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
197 }
198 return mergedHistory;
199 }
200 }
201
202 public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
203 @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
204 synchronized (mLock) {
205 final NotificationHistoryDatabase userHistory =
206 getUserHistoryAndInitializeIfNeededLocked(userId);
207 if (userHistory == null) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500208 Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId);
Julia Reynoldsa614c272019-11-15 16:43:29 -0500209 return new android.app.NotificationHistory();
210 }
211
212 return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
213 }
214 }
215
Julia Reynoldsb317ff72019-11-26 14:20:51 -0500216 boolean isHistoryEnabled(@UserIdInt int userId) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500217 synchronized (mLock) {
218 return mHistoryEnabled.get(userId);
219 }
220 }
221
222 void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) {
223 synchronized (mLock) {
Julia Reynolds6c111032020-01-17 12:32:59 -0500224 if (historyEnabled) {
225 mHistoryEnabled.put(userId, historyEnabled);
226 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500227 final NotificationHistoryDatabase userHistory =
228 getUserHistoryAndInitializeIfNeededLocked(userId);
229 if (userHistory != null) {
230 if (!historyEnabled) {
Julia Reynolds6c111032020-01-17 12:32:59 -0500231 disableHistory(userHistory, userId);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500232 }
Julia Reynolds6c111032020-01-17 12:32:59 -0500233 } else {
234 mUserPendingHistoryDisables.put(userId, !historyEnabled);
Julia Reynoldse43fac32019-11-20 13:36:24 -0500235 }
236 }
237 }
238
Julia Reynolds6c111032020-01-17 12:32:59 -0500239 private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
240 userHistory.disableHistory();
241
242 mUserPendingHistoryDisables.put(userId, false);
243 mHistoryEnabled.put(userId, false);
244 mUserState.put(userId, null);
245 }
246
Julia Reynoldsa614c272019-11-15 16:43:29 -0500247 @GuardedBy("mLock")
248 private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
249 int userId) {
Julia Reynoldse43fac32019-11-20 13:36:24 -0500250 if (!mHistoryEnabled.get(userId)) {
251 if (DEBUG) {
252 Slog.i(TAG, "History disabled for user " + userId);
253 }
254 mUserState.put(userId, null);
255 return null;
256 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500257 NotificationHistoryDatabase userHistory = mUserState.get(userId);
258 if (userHistory == null) {
259 final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
260 DIRECTORY_PER_USER);
261 userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
262 historyDir, new NotificationHistoryFileAttrProvider());
263 if (mUserUnlockedStates.get(userId)) {
264 try {
265 userHistory.init();
266 } catch (Exception e) {
267 if (mUserManager.isUserUnlocked(userId)) {
268 throw e; // rethrow exception - user is unlocked
269 } else {
270 Slog.w(TAG, "Attempted to initialize service for "
271 + "stopped or removed user " + userId);
272 return null;
273 }
274 }
275 } else {
276 // locked! data unavailable
277 Slog.w(TAG, "Attempted to initialize service for "
278 + "stopped or removed user " + userId);
279 return null;
280 }
281 mUserState.put(userId, userHistory);
282 }
283 return userHistory;
284 }
285
286 @VisibleForTesting
287 boolean isUserUnlocked(@UserIdInt int userId) {
288 synchronized (mLock) {
289 return mUserUnlockedStates.get(userId);
290 }
291 }
292
293 @VisibleForTesting
294 boolean doesHistoryExistForUser(@UserIdInt int userId) {
295 synchronized (mLock) {
296 return mUserState.get(userId) != null;
297 }
298 }
299
300 @VisibleForTesting
301 void replaceNotificationHistoryDatabase(@UserIdInt int userId,
302 NotificationHistoryDatabase replacement) {
303 synchronized (mLock) {
304 if (mUserState.get(userId) != null) {
305 mUserState.put(userId, replacement);
306 }
307 }
308 }
309
310 @VisibleForTesting
311 List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
312 synchronized (mLock) {
313 return mUserPendingPackageRemovals.get(userId);
314 }
315 }
Julia Reynoldse43fac32019-11-20 13:36:24 -0500316
317 final class SettingsObserver extends ContentObserver {
318 private final Uri NOTIFICATION_HISTORY_URI
319 = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED);
320
321 SettingsObserver(Handler handler) {
322 super(handler);
323 }
324
325 void observe() {
326 ContentResolver resolver = mContext.getContentResolver();
327 resolver.registerContentObserver(NOTIFICATION_HISTORY_URI,
328 false, this, UserHandle.USER_ALL);
329 synchronized (mLock) {
330 for (UserInfo userInfo : mUserManager.getUsers()) {
331 update(null, userInfo.id);
332 }
333 }
334 }
335
Julia Reynolds6c111032020-01-17 12:32:59 -0500336 void stopObserving() {
337 ContentResolver resolver = mContext.getContentResolver();
338 resolver.unregisterContentObserver(this);
339 }
340
Julia Reynoldse43fac32019-11-20 13:36:24 -0500341 @Override
342 public void onChange(boolean selfChange, Uri uri, int userId) {
343 update(uri, userId);
344 }
345
346 public void update(Uri uri, int userId) {
347 ContentResolver resolver = mContext.getContentResolver();
348 if (uri == null || NOTIFICATION_HISTORY_URI.equals(uri)) {
349 boolean historyEnabled = Settings.Secure.getIntForUser(resolver,
350 Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId)
351 != 0;
352 onHistoryEnabledChanged(userId, historyEnabled);
353 }
354 }
355 }
Julia Reynoldsa614c272019-11-15 16:43:29 -0500356}