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.app.NotificationHistory; 20 import android.app.NotificationHistory.HistoricalNotification; 21 import android.os.Handler; 22 import android.util.AtomicFile; 23 import android.util.Slog; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 27 import java.io.BufferedReader; 28 import java.io.BufferedWriter; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileNotFoundException; 32 import java.io.FileOutputStream; 33 import java.io.FileReader; 34 import java.io.FileWriter; 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Calendar; 39 import java.util.GregorianCalendar; 40 import java.util.Iterator; 41 import java.util.List; 42 import java.util.Set; 43 44 /** 45 * Provides an interface to write and query for notification history data for a user from a Protocol 46 * Buffer database. 47 * 48 * Periodically writes the buffered history to disk but can also accept force writes based on 49 * outside changes (like a pending shutdown). 50 */ 51 public class NotificationHistoryDatabase { 52 private static final int DEFAULT_CURRENT_VERSION = 1; 53 54 private static final String TAG = "NotiHistoryDatabase"; 55 private static final boolean DEBUG = NotificationManagerService.DBG; 56 private static final int HISTORY_RETENTION_DAYS = 1; 57 private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20; 58 private static final long INVALID_FILE_TIME_MS = -1; 59 60 private final Object mLock = new Object(); 61 private final Handler mFileWriteHandler; 62 @VisibleForTesting 63 // List of files holding history information, sorted newest to oldest 64 final List<AtomicFile> mHistoryFiles; 65 private final File mHistoryDir; 66 private final File mVersionFile; 67 // Current version of the database files schema 68 private int mCurrentVersion; 69 private final WriteBufferRunnable mWriteBufferRunnable; 70 71 // Object containing posted notifications that have not yet been written to disk 72 @VisibleForTesting 73 NotificationHistory mBuffer; 74 NotificationHistoryDatabase(Handler fileWriteHandler, File dir)75 public NotificationHistoryDatabase(Handler fileWriteHandler, File dir) { 76 mCurrentVersion = DEFAULT_CURRENT_VERSION; 77 mFileWriteHandler = fileWriteHandler; 78 mVersionFile = new File(dir, "version"); 79 mHistoryDir = new File(dir, "history"); 80 mHistoryFiles = new ArrayList<>(); 81 mBuffer = new NotificationHistory(); 82 mWriteBufferRunnable = new WriteBufferRunnable(); 83 } 84 init()85 public void init() { 86 synchronized (mLock) { 87 try { 88 if (!mHistoryDir.exists() && !mHistoryDir.mkdir()) { 89 throw new IllegalStateException("could not create history directory"); 90 } 91 mVersionFile.createNewFile(); 92 } catch (Exception e) { 93 Slog.e(TAG, "could not create needed files", e); 94 } 95 96 checkVersionAndBuildLocked(); 97 indexFilesLocked(); 98 prune(); 99 } 100 } 101 indexFilesLocked()102 private void indexFilesLocked() { 103 mHistoryFiles.clear(); 104 final File[] files = mHistoryDir.listFiles(); 105 if (files == null) { 106 return; 107 } 108 109 // Sort with newest files first 110 Arrays.sort(files, (lhs, rhs) -> Long.compare(safeParseLong(rhs.getName()), 111 safeParseLong(lhs.getName()))); 112 113 for (File file : files) { 114 mHistoryFiles.add(new AtomicFile(file)); 115 } 116 } 117 checkVersionAndBuildLocked()118 private void checkVersionAndBuildLocked() { 119 int version; 120 try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) { 121 version = Integer.parseInt(reader.readLine()); 122 } catch (NumberFormatException | IOException e) { 123 version = 0; 124 } 125 126 if (version != mCurrentVersion && mVersionFile.exists()) { 127 try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) { 128 writer.write(Integer.toString(mCurrentVersion)); 129 writer.write("\n"); 130 writer.flush(); 131 } catch (IOException e) { 132 Slog.e(TAG, "Failed to write new version"); 133 throw new RuntimeException(e); 134 } 135 } 136 } 137 forceWriteToDisk()138 public void forceWriteToDisk() { 139 mFileWriteHandler.post(mWriteBufferRunnable); 140 } 141 onPackageRemoved(String packageName)142 public void onPackageRemoved(String packageName) { 143 RemovePackageRunnable rpr = new RemovePackageRunnable(packageName); 144 mFileWriteHandler.post(rpr); 145 } 146 deleteNotificationHistoryItem(String pkg, long postedTime)147 public void deleteNotificationHistoryItem(String pkg, long postedTime) { 148 RemoveNotificationRunnable rnr = new RemoveNotificationRunnable(pkg, postedTime); 149 mFileWriteHandler.post(rnr); 150 } 151 deleteConversations(String pkg, Set<String> conversationIds)152 public void deleteConversations(String pkg, Set<String> conversationIds) { 153 RemoveConversationRunnable rcr = new RemoveConversationRunnable(pkg, conversationIds); 154 mFileWriteHandler.post(rcr); 155 } 156 deleteNotificationChannel(String pkg, String channelId)157 public void deleteNotificationChannel(String pkg, String channelId) { 158 RemoveChannelRunnable rcr = new RemoveChannelRunnable(pkg, channelId); 159 mFileWriteHandler.post(rcr); 160 } 161 addNotification(final HistoricalNotification notification)162 public void addNotification(final HistoricalNotification notification) { 163 synchronized (mLock) { 164 mBuffer.addNewNotificationToWrite(notification); 165 // Each time we have new history to write to disk, schedule a write in [interval] ms 166 if (mBuffer.getHistoryCount() == 1) { 167 mFileWriteHandler.postDelayed(mWriteBufferRunnable, WRITE_BUFFER_INTERVAL_MS); 168 } 169 } 170 } 171 readNotificationHistory()172 public NotificationHistory readNotificationHistory() { 173 synchronized (mLock) { 174 NotificationHistory notifications = new NotificationHistory(); 175 notifications.addNotificationsToWrite(mBuffer); 176 177 for (AtomicFile file : mHistoryFiles) { 178 try { 179 readLocked( 180 file, notifications, new NotificationHistoryFilter.Builder().build()); 181 } catch (Exception e) { 182 Slog.e(TAG, "error reading " + file.getBaseFile().getAbsolutePath(), e); 183 } 184 } 185 186 return notifications; 187 } 188 } 189 readNotificationHistory(String packageName, String channelId, int maxNotifications)190 public NotificationHistory readNotificationHistory(String packageName, String channelId, 191 int maxNotifications) { 192 synchronized (mLock) { 193 NotificationHistory notifications = new NotificationHistory(); 194 195 for (AtomicFile file : mHistoryFiles) { 196 try { 197 readLocked(file, notifications, 198 new NotificationHistoryFilter.Builder() 199 .setPackage(packageName) 200 .setChannel(packageName, channelId) 201 .setMaxNotifications(maxNotifications) 202 .build()); 203 if (maxNotifications == notifications.getHistoryCount()) { 204 // No need to read any more files 205 break; 206 } 207 } catch (Exception e) { 208 Slog.e(TAG, "error reading " + file.getBaseFile().getAbsolutePath(), e); 209 } 210 } 211 212 return notifications; 213 } 214 } 215 disableHistory()216 public void disableHistory() { 217 synchronized (mLock) { 218 for (AtomicFile file : mHistoryFiles) { 219 file.delete(); 220 } 221 mHistoryDir.delete(); 222 mHistoryFiles.clear(); 223 } 224 } 225 226 /** 227 * Remove any files that are too old. 228 */ prune()229 void prune() { 230 prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis()); 231 } 232 233 /** 234 * Remove any files that are too old. 235 */ prune(final int retentionDays, final long currentTimeMillis)236 void prune(final int retentionDays, final long currentTimeMillis) { 237 synchronized (mLock) { 238 GregorianCalendar retentionBoundary = new GregorianCalendar(); 239 retentionBoundary.setTimeInMillis(currentTimeMillis); 240 retentionBoundary.add(Calendar.DATE, -1 * retentionDays); 241 242 for (int i = mHistoryFiles.size() - 1; i >= 0; i--) { 243 final AtomicFile currentOldestFile = mHistoryFiles.get(i); 244 final long creationTime = safeParseLong( 245 currentOldestFile.getBaseFile().getName()); 246 if (DEBUG) { 247 Slog.d(TAG, "File " + currentOldestFile.getBaseFile().getName() 248 + " created on " + creationTime); 249 } 250 251 if (creationTime <= retentionBoundary.getTimeInMillis()) { 252 deleteFile(currentOldestFile); 253 } 254 } 255 } 256 } 257 258 /** 259 * Remove the first entry from the list of history files whose file matches the given file path. 260 * 261 * This method is necessary for anything that only has an absolute file path rather than an 262 * AtomicFile object from the list of history files. 263 * 264 * filePath should be an absolute path. 265 */ removeFilePathFromHistory(String filePath)266 void removeFilePathFromHistory(String filePath) { 267 if (filePath == null) { 268 return; 269 } 270 271 Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); 272 while (historyFileItr.hasNext()) { 273 final AtomicFile af = historyFileItr.next(); 274 if (af != null && filePath.equals(af.getBaseFile().getAbsolutePath())) { 275 historyFileItr.remove(); 276 return; 277 } 278 } 279 } 280 deleteFile(AtomicFile file)281 private void deleteFile(AtomicFile file) { 282 if (DEBUG) { 283 Slog.d(TAG, "Removed " + file.getBaseFile().getName()); 284 } 285 file.delete(); 286 // TODO: delete all relevant bitmaps, once they exist 287 removeFilePathFromHistory(file.getBaseFile().getAbsolutePath()); 288 } 289 writeLocked(AtomicFile file, NotificationHistory notifications)290 private void writeLocked(AtomicFile file, NotificationHistory notifications) 291 throws IOException { 292 FileOutputStream fos = file.startWrite(); 293 try { 294 NotificationHistoryProtoHelper.write(fos, notifications, mCurrentVersion); 295 file.finishWrite(fos); 296 fos = null; 297 } finally { 298 // When fos is null (successful write), this will no-op 299 file.failWrite(fos); 300 } 301 } 302 readLocked(AtomicFile file, NotificationHistory notificationsOut, NotificationHistoryFilter filter)303 private static void readLocked(AtomicFile file, NotificationHistory notificationsOut, 304 NotificationHistoryFilter filter) throws IOException { 305 FileInputStream in = null; 306 try { 307 in = file.openRead(); 308 NotificationHistoryProtoHelper.read(in, notificationsOut, filter); 309 } catch (FileNotFoundException e) { 310 Slog.e(TAG, "Cannot open " + file.getBaseFile().getAbsolutePath(), e); 311 throw e; 312 } finally { 313 if (in != null) { 314 in.close(); 315 } 316 } 317 } 318 safeParseLong(String fileName)319 private static long safeParseLong(String fileName) { 320 // AtomicFile will create copies of the numeric files with ".new" and ".bak" 321 // over the course of its processing. If these files still exist on boot we need to clean 322 // them up 323 try { 324 return Long.parseLong(fileName); 325 } catch (NumberFormatException e) { 326 return INVALID_FILE_TIME_MS; 327 } 328 } 329 330 final class WriteBufferRunnable implements Runnable { 331 332 @Override run()333 public void run() { 334 long time = System.currentTimeMillis(); 335 run(new AtomicFile(new File(mHistoryDir, String.valueOf(time)))); 336 } 337 run(AtomicFile file)338 void run(AtomicFile file) { 339 synchronized (mLock) { 340 if (DEBUG) Slog.d(TAG, "WriteBufferRunnable " 341 + file.getBaseFile().getAbsolutePath()); 342 try { 343 writeLocked(file, mBuffer); 344 mHistoryFiles.add(0, file); 345 mBuffer = new NotificationHistory(); 346 } catch (IOException e) { 347 Slog.e(TAG, "Failed to write buffer to disk. not flushing buffer", e); 348 } 349 } 350 } 351 } 352 353 private final class RemovePackageRunnable implements Runnable { 354 private String mPkg; 355 RemovePackageRunnable(String pkg)356 public RemovePackageRunnable(String pkg) { 357 mPkg = pkg; 358 } 359 360 @Override run()361 public void run() { 362 if (DEBUG) Slog.d(TAG, "RemovePackageRunnable " + mPkg); 363 synchronized (mLock) { 364 // Remove packageName entries from pending history 365 mBuffer.removeNotificationsFromWrite(mPkg); 366 367 Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); 368 while (historyFileItr.hasNext()) { 369 final AtomicFile af = historyFileItr.next(); 370 try { 371 final NotificationHistory notifications = new NotificationHistory(); 372 readLocked(af, notifications, 373 new NotificationHistoryFilter.Builder().build()); 374 notifications.removeNotificationsFromWrite(mPkg); 375 writeLocked(af, notifications); 376 } catch (Exception e) { 377 Slog.e(TAG, "Cannot clean up file on pkg removal " 378 + af.getBaseFile().getAbsolutePath(), e); 379 } 380 } 381 } 382 } 383 } 384 385 final class RemoveNotificationRunnable implements Runnable { 386 private String mPkg; 387 private long mPostedTime; 388 private NotificationHistory mNotificationHistory; 389 RemoveNotificationRunnable(String pkg, long postedTime)390 public RemoveNotificationRunnable(String pkg, long postedTime) { 391 mPkg = pkg; 392 mPostedTime = postedTime; 393 } 394 395 @VisibleForTesting setNotificationHistory(NotificationHistory nh)396 void setNotificationHistory(NotificationHistory nh) { 397 mNotificationHistory = nh; 398 } 399 400 @Override run()401 public void run() { 402 if (DEBUG) Slog.d(TAG, "RemoveNotificationRunnable"); 403 synchronized (mLock) { 404 // Remove from pending history 405 mBuffer.removeNotificationFromWrite(mPkg, mPostedTime); 406 407 Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); 408 while (historyFileItr.hasNext()) { 409 final AtomicFile af = historyFileItr.next(); 410 try { 411 NotificationHistory notificationHistory = mNotificationHistory != null 412 ? mNotificationHistory 413 : new NotificationHistory(); 414 readLocked(af, notificationHistory, 415 new NotificationHistoryFilter.Builder().build()); 416 if(notificationHistory.removeNotificationFromWrite(mPkg, mPostedTime)) { 417 writeLocked(af, notificationHistory); 418 } 419 } catch (Exception e) { 420 Slog.e(TAG, "Cannot clean up file on notification removal " 421 + af.getBaseFile().getName(), e); 422 } 423 } 424 } 425 } 426 } 427 428 final class RemoveConversationRunnable implements Runnable { 429 private String mPkg; 430 private Set<String> mConversationIds; 431 private NotificationHistory mNotificationHistory; 432 RemoveConversationRunnable(String pkg, Set<String> conversationIds)433 public RemoveConversationRunnable(String pkg, Set<String> conversationIds) { 434 mPkg = pkg; 435 mConversationIds = conversationIds; 436 } 437 438 @VisibleForTesting setNotificationHistory(NotificationHistory nh)439 void setNotificationHistory(NotificationHistory nh) { 440 mNotificationHistory = nh; 441 } 442 443 @Override run()444 public void run() { 445 if (DEBUG) Slog.d(TAG, "RemoveConversationRunnable " + mPkg + " " + mConversationIds); 446 synchronized (mLock) { 447 // Remove from pending history 448 mBuffer.removeConversationsFromWrite(mPkg, mConversationIds); 449 450 Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); 451 while (historyFileItr.hasNext()) { 452 final AtomicFile af = historyFileItr.next(); 453 try { 454 NotificationHistory notificationHistory = mNotificationHistory != null 455 ? mNotificationHistory 456 : new NotificationHistory(); 457 readLocked(af, notificationHistory, 458 new NotificationHistoryFilter.Builder().build()); 459 if (notificationHistory.removeConversationsFromWrite( 460 mPkg, mConversationIds)) { 461 writeLocked(af, notificationHistory); 462 } 463 } catch (Exception e) { 464 Slog.e(TAG, "Cannot clean up file on conversation removal " 465 + af.getBaseFile().getName(), e); 466 } 467 } 468 } 469 } 470 } 471 472 final class RemoveChannelRunnable implements Runnable { 473 private String mPkg; 474 private String mChannelId; 475 private NotificationHistory mNotificationHistory; 476 RemoveChannelRunnable(String pkg, String channelId)477 RemoveChannelRunnable(String pkg, String channelId) { 478 mPkg = pkg; 479 mChannelId = channelId; 480 } 481 482 @VisibleForTesting setNotificationHistory(NotificationHistory nh)483 void setNotificationHistory(NotificationHistory nh) { 484 mNotificationHistory = nh; 485 } 486 487 @Override run()488 public void run() { 489 if (DEBUG) Slog.d(TAG, "RemoveChannelRunnable"); 490 synchronized (mLock) { 491 // Remove from pending history 492 mBuffer.removeChannelFromWrite(mPkg, mChannelId); 493 494 Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); 495 while (historyFileItr.hasNext()) { 496 final AtomicFile af = historyFileItr.next(); 497 try { 498 NotificationHistory notificationHistory = mNotificationHistory != null 499 ? mNotificationHistory 500 : new NotificationHistory(); 501 readLocked(af, notificationHistory, 502 new NotificationHistoryFilter.Builder().build()); 503 if (notificationHistory.removeChannelFromWrite(mPkg, mChannelId)) { 504 writeLocked(af, notificationHistory); 505 } 506 } catch (Exception e) { 507 Slog.e(TAG, "Cannot clean up file on channel removal " 508 + af.getBaseFile().getName(), e); 509 } 510 } 511 } 512 } 513 } 514 } 515