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