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