• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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