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