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