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