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