• 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.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