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