Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 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; |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 24 | import android.content.ContentResolver; |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 25 | import android.content.Context; |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 26 | import android.content.pm.UserInfo; |
| 27 | import android.database.ContentObserver; |
| 28 | import android.net.Uri; |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 29 | import android.os.Environment; |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 30 | import android.os.Handler; |
| 31 | import android.os.UserHandle; |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 32 | import android.os.UserManager; |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 33 | import android.provider.Settings; |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 34 | import android.util.Slog; |
| 35 | import android.util.SparseArray; |
| 36 | import android.util.SparseBooleanArray; |
| 37 | |
| 38 | import com.android.internal.annotations.GuardedBy; |
| 39 | import com.android.internal.annotations.VisibleForTesting; |
| 40 | import com.android.server.IoThread; |
| 41 | import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider; |
| 42 | |
| 43 | import java.io.File; |
| 44 | import java.util.ArrayList; |
| 45 | import java.util.List; |
| 46 | |
| 47 | /** |
| 48 | * Keeps track of per-user notification histories. |
| 49 | */ |
| 50 | public 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 59 | @VisibleForTesting |
| 60 | final SettingsObserver mSettingsObserver; |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 61 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 69 | @GuardedBy("mLock") |
| 70 | private final SparseBooleanArray mHistoryEnabled = new SparseBooleanArray(); |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 71 | @GuardedBy("mLock") |
| 72 | private final SparseBooleanArray mUserPendingHistoryDisables = new SparseBooleanArray(); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 73 | |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 74 | public NotificationHistoryManager(Context context, Handler handler) { |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 75 | mContext = context; |
| 76 | mUserManager = context.getSystemService(UserManager.class); |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 77 | mSettingsObserver = new SettingsObserver(handler); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 78 | } |
| 79 | |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 80 | @VisibleForTesting |
| 81 | void onDestroy() { |
| 82 | mSettingsObserver.stopObserving(); |
| 83 | } |
| 84 | |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 85 | void onBootPhaseAppsCanStart() { |
| 86 | mSettingsObserver.observe(); |
| 87 | } |
| 88 | |
| 89 | void onUserUnlocked(@UserIdInt int userId) { |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 90 | synchronized (mLock) { |
| 91 | mUserUnlockedStates.put(userId, true); |
| 92 | final NotificationHistoryDatabase userHistory = |
| 93 | getUserHistoryAndInitializeIfNeededLocked(userId); |
| 94 | if (userHistory == null) { |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 95 | Slog.i(TAG, "Attempted to unlock gone/disabled user " + userId); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 96 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 107 | |
| 108 | // delete history if it was disabled when the user was locked |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 109 | if (mUserPendingHistoryDisables.get(userId)) { |
| 110 | disableHistory(userHistory, userId); |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 111 | } |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 112 | } |
| 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 Reynolds | b317ff7 | 2019-11-26 14:20:51 -0500 | [diff] [blame] | 122 | public void onUserRemoved(@UserIdInt int userId) { |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 123 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 127 | mHistoryEnabled.put(userId, false); |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 128 | mUserPendingHistoryDisables.put(userId, false); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 129 | onUserStopped(userId); |
| 130 | } |
| 131 | } |
| 132 | |
Julia Reynolds | b317ff7 | 2019-11-26 14:20:51 -0500 | [diff] [blame] | 133 | public void onPackageRemoved(int userId, String packageName) { |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 134 | synchronized (mLock) { |
| 135 | if (!mUserUnlockedStates.get(userId, false)) { |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 136 | 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 Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 142 | 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 Reynolds | b317ff7 | 2019-11-26 14:20:51 -0500 | [diff] [blame] | 153 | // TODO: wire this up to AMS when power button is long pressed |
| 154 | public void triggerWriteToDisk() { |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 155 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 175 | Slog.w(TAG, "Attempted to add notif for locked/gone/disabled user " |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 176 | + 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 193 | Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 194 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 208 | Slog.i(TAG, "Attempted to read history for locked/gone/disabled user " +userId); |
Julia Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 209 | return new android.app.NotificationHistory(); |
| 210 | } |
| 211 | |
| 212 | return userHistory.readNotificationHistory(packageName, channelId, maxNotifications); |
| 213 | } |
| 214 | } |
| 215 | |
Julia Reynolds | b317ff7 | 2019-11-26 14:20:51 -0500 | [diff] [blame] | 216 | boolean isHistoryEnabled(@UserIdInt int userId) { |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 217 | synchronized (mLock) { |
| 218 | return mHistoryEnabled.get(userId); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | void onHistoryEnabledChanged(@UserIdInt int userId, boolean historyEnabled) { |
| 223 | synchronized (mLock) { |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 224 | if (historyEnabled) { |
| 225 | mHistoryEnabled.put(userId, historyEnabled); |
| 226 | } |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 227 | final NotificationHistoryDatabase userHistory = |
| 228 | getUserHistoryAndInitializeIfNeededLocked(userId); |
| 229 | if (userHistory != null) { |
| 230 | if (!historyEnabled) { |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 231 | disableHistory(userHistory, userId); |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 232 | } |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 233 | } else { |
| 234 | mUserPendingHistoryDisables.put(userId, !historyEnabled); |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
Julia Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 239 | 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 Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 247 | @GuardedBy("mLock") |
| 248 | private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked( |
| 249 | int userId) { |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 250 | 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 Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 257 | 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 Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 316 | |
| 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 Reynolds | 6c11103 | 2020-01-17 12:32:59 -0500 | [diff] [blame] | 336 | void stopObserving() { |
| 337 | ContentResolver resolver = mContext.getContentResolver(); |
| 338 | resolver.unregisterContentObserver(this); |
| 339 | } |
| 340 | |
Julia Reynolds | e43fac3 | 2019-11-20 13:36:24 -0500 | [diff] [blame] | 341 | @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 Reynolds | a614c27 | 2019-11-15 16:43:29 -0500 | [diff] [blame] | 356 | } |