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