Loading services/core/java/com/android/server/notification/NotificationHistoryDatabase.java +100 −19 Original line number Original line Diff line number Diff line Loading @@ -16,8 +16,15 @@ package com.android.server.notification; package com.android.server.notification; import android.app.AlarmManager; import android.app.NotificationHistory; import android.app.NotificationHistory; import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationHistory.HistoricalNotification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; import android.os.Handler; import android.util.AtomicFile; import android.util.AtomicFile; import android.util.Slog; import android.util.Slog; Loading @@ -33,11 +40,15 @@ import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileReader; import java.io.FileWriter; import java.io.FileWriter; import java.io.IOException; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Arrays; import java.util.Calendar; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.Iterator; import java.util.LinkedList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; /** /** * Provides an interface to write and query for notification history data for a user from a Protocol * Provides an interface to write and query for notification history data for a user from a Protocol Loading @@ -52,32 +63,48 @@ public class NotificationHistoryDatabase { private static final String TAG = "NotiHistoryDatabase"; private static final String TAG = "NotiHistoryDatabase"; private static final boolean DEBUG = NotificationManagerService.DBG; private static final boolean DEBUG = NotificationManagerService.DBG; private static final int HISTORY_RETENTION_DAYS = 2; private static final int HISTORY_RETENTION_DAYS = 2; private static final int HISTORY_RETENTION_MS = 24 * 60 * 60 * 1000; private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20; private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20; private static final String ACTION_HISTORY_DELETION = NotificationHistoryDatabase.class.getSimpleName() + ".CLEANUP"; private static final int REQUEST_CODE_DELETION = 1; private static final String SCHEME_DELETION = "delete"; private static final String EXTRA_KEY = "key"; private final Context mContext; private final AlarmManager mAlarmManager; private final Object mLock = new Object(); private final Object mLock = new Object(); private Handler mFileWriteHandler; private Handler mFileWriteHandler; @VisibleForTesting @VisibleForTesting // List of files holding history information, sorted newest to oldest // List of files holding history information, sorted newest to oldest final LinkedList<AtomicFile> mHistoryFiles; final LinkedList<AtomicFile> mHistoryFiles; private final GregorianCalendar mCal; private final File mHistoryDir; private final File mHistoryDir; private final File mVersionFile; private final File mVersionFile; // Current version of the database files schema // Current version of the database files schema private int mCurrentVersion; private int mCurrentVersion; private final WriteBufferRunnable mWriteBufferRunnable; private final WriteBufferRunnable mWriteBufferRunnable; private final FileAttrProvider mFileAttrProvider; // Object containing posted notifications that have not yet been written to disk // Object containing posted notifications that have not yet been written to disk @VisibleForTesting @VisibleForTesting NotificationHistory mBuffer; NotificationHistory mBuffer; public NotificationHistoryDatabase(File dir) { public NotificationHistoryDatabase(Context context, File dir, FileAttrProvider fileAttrProvider) { mContext = context; mAlarmManager = context.getSystemService(AlarmManager.class); mCurrentVersion = DEFAULT_CURRENT_VERSION; mCurrentVersion = DEFAULT_CURRENT_VERSION; mVersionFile = new File(dir, "version"); mVersionFile = new File(dir, "version"); mHistoryDir = new File(dir, "history"); mHistoryDir = new File(dir, "history"); mHistoryFiles = new LinkedList<>(); mHistoryFiles = new LinkedList<>(); mCal = new GregorianCalendar(); mBuffer = new NotificationHistory(); mBuffer = new NotificationHistory(); mWriteBufferRunnable = new WriteBufferRunnable(); mWriteBufferRunnable = new WriteBufferRunnable(); mFileAttrProvider = fileAttrProvider; IntentFilter deletionFilter = new IntentFilter(ACTION_HISTORY_DELETION); deletionFilter.addDataScheme(SCHEME_DELETION); mContext.registerReceiver(mFileCleaupReceiver, deletionFilter); } } public void init(Handler fileWriteHandler) { public void init(Handler fileWriteHandler) { Loading Loading @@ -105,7 +132,8 @@ public class NotificationHistoryDatabase { } } // Sort with newest files first // Sort with newest files first Arrays.sort(files, (lhs, rhs) -> Long.compare(rhs.lastModified(), lhs.lastModified())); Arrays.sort(files, (lhs, rhs) -> Long.compare(mFileAttrProvider.getCreationTime(rhs), mFileAttrProvider.getCreationTime(lhs))); for (File file : files) { for (File file : files) { mHistoryFiles.addLast(new AtomicFile(file)); mHistoryFiles.addLast(new AtomicFile(file)); Loading Loading @@ -197,29 +225,46 @@ public class NotificationHistoryDatabase { } } /** /** * Remove any files that are too old. * Remove any files that are too old and schedule jobs to clean up the rest */ */ public void prune(final int retentionDays, final long currentTimeMillis) { public void prune(final int retentionDays, final long currentTimeMillis) { synchronized (mLock) { synchronized (mLock) { mCal.setTimeInMillis(currentTimeMillis); GregorianCalendar retentionBoundary = new GregorianCalendar(); mCal.add(Calendar.DATE, -1 * retentionDays); retentionBoundary.setTimeInMillis(currentTimeMillis); retentionBoundary.add(Calendar.DATE, -1 * retentionDays); while (!mHistoryFiles.isEmpty()) { final AtomicFile currentOldestFile = mHistoryFiles.getLast(); for (int i = mHistoryFiles.size() - 1; i >= 0; i--) { final long age = currentTimeMillis final AtomicFile currentOldestFile = mHistoryFiles.get(i); - currentOldestFile.getBaseFile().lastModified(); final long creationTime = if (age > mCal.getTimeInMillis()) { mFileAttrProvider.getCreationTime(currentOldestFile.getBaseFile()); if (creationTime <= retentionBoundary.getTimeInMillis()) { if (DEBUG) { if (DEBUG) { Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName()); Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName()); } } currentOldestFile.delete(); currentOldestFile.delete(); mHistoryFiles.removeLast(); mHistoryFiles.removeLast(); } else { } else { // all remaining files are newer than the cut off // all remaining files are newer than the cut off; schedule jobs to delete return; final long deletionTime = creationTime + (retentionDays * HISTORY_RETENTION_MS); scheduleDeletion(currentOldestFile.getBaseFile(), deletionTime); } } } } } } } void scheduleDeletion(File file, long deletionTime) { if (DEBUG) { Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime); } final PendingIntent pi = PendingIntent.getBroadcast(mContext, REQUEST_CODE_DELETION, new Intent(ACTION_HISTORY_DELETION) .setData(new Uri.Builder().scheme(SCHEME_DELETION) .appendPath(file.getAbsolutePath()).build()) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) .putExtra(EXTRA_KEY, file.getAbsolutePath()), PendingIntent.FLAG_UPDATE_CURRENT); mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, deletionTime, pi); } } private void writeLocked(AtomicFile file, NotificationHistory notifications) private void writeLocked(AtomicFile file, NotificationHistory notifications) Loading @@ -245,6 +290,25 @@ public class NotificationHistoryDatabase { } } } } private final BroadcastReceiver mFileCleaupReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action == null) { return; } if (ACTION_HISTORY_DELETION.equals(action)) { try { final String filePath = intent.getStringExtra(EXTRA_KEY); AtomicFile fileToDelete = new AtomicFile(new File(filePath)); fileToDelete.delete(); } catch (Exception e) { Slog.e(TAG, "Failed to delete notification history file", e); } } } }; private final class WriteBufferRunnable implements Runnable { private final class WriteBufferRunnable implements Runnable { @Override @Override public void run() { public void run() { Loading Loading @@ -277,10 +341,7 @@ public class NotificationHistoryDatabase { // Remove packageName entries from pending history // Remove packageName entries from pending history mBuffer.removeNotificationsFromWrite(mPkg); mBuffer.removeNotificationsFromWrite(mPkg); // Remove packageName entries from files on disk, and rewrite them to disk Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); // Since we sort by modified date, we have to update the files oldest to newest to // maintain the original ordering Iterator<AtomicFile> historyFileItr = mHistoryFiles.descendingIterator(); while (historyFileItr.hasNext()) { while (historyFileItr.hasNext()) { final AtomicFile af = historyFileItr.next(); final AtomicFile af = historyFileItr.next(); try { try { Loading @@ -297,4 +358,24 @@ public class NotificationHistoryDatabase { } } } } } } public static final class NotificationHistoryFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider { final static String TAG = "NotifHistoryFileDate"; public long getCreationTime(File file) { try { BasicFileAttributes attr = Files.readAttributes(FileSystems.getDefault().getPath( file.getAbsolutePath()), BasicFileAttributes.class); return attr.creationTime().to(TimeUnit.MILLISECONDS); } catch (Exception e) { Slog.w(TAG, "Cannot read creation data for file; using file name"); return Long.valueOf(file.getName()); } } } interface FileAttrProvider { long getCreationTime(File file); } } } services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java +56 −11 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.notification; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.never; Loading @@ -25,7 +26,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationHistory.HistoricalNotification; import android.content.Context; import android.graphics.drawable.Icon; import android.graphics.drawable.Icon; import android.os.Handler; import android.os.Handler; import android.util.AtomicFile; import android.util.AtomicFile; Loading @@ -42,8 +45,17 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations; import java.io.File; import java.io.File; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Calendar; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class) public class NotificationHistoryDatabaseTest extends UiServiceTestCase { public class NotificationHistoryDatabaseTest extends UiServiceTestCase { Loading @@ -51,6 +63,11 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { File mRootDir; File mRootDir; @Mock @Mock Handler mFileWriteHandler; Handler mFileWriteHandler; @Mock Context mContext; @Mock AlarmManager mAlarmManager; TestFileAttrProvider mFileAttrProvider; NotificationHistoryDatabase mDataBase; NotificationHistoryDatabase mDataBase; Loading Loading @@ -85,36 +102,56 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { @Before @Before public void setUp() { public void setUp() { MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this); when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); when(mContext.getUser()).thenReturn(getContext().getUser()); when(mContext.getPackageName()).thenReturn(getContext().getPackageName()); mFileAttrProvider = new TestFileAttrProvider(); mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); mDataBase = new NotificationHistoryDatabase(mRootDir); mDataBase = new NotificationHistoryDatabase(mContext, mRootDir, mFileAttrProvider); mDataBase.init(mFileWriteHandler); mDataBase.init(mFileWriteHandler); } } @Test @Test public void testPrune() { public void testDeletionReceiver() { verify(mContext, times(1)).registerReceiver(any(), any()); } @Test public void testPrune() throws Exception { GregorianCalendar cal = new GregorianCalendar(); cal.setTimeInMillis(10); int retainDays = 1; int retainDays = 1; for (long i = 10; i >= 5; i--) { List<AtomicFile> expectedFiles = new ArrayList<>(); // add 5 files with a creation date of "today" for (long i = cal.getTimeInMillis(); i >= 5; i--) { File file = mock(File.class); File file = mock(File.class); when(file.lastModified()).thenReturn(i); mFileAttrProvider.creationDates.put(file, i); AtomicFile af = new AtomicFile(file); AtomicFile af = new AtomicFile(file); expectedFiles.add(af); mDataBase.mHistoryFiles.addLast(af); mDataBase.mHistoryFiles.addLast(af); } } GregorianCalendar cal = new GregorianCalendar(); cal.setTimeInMillis(5); cal.add(Calendar.DATE, -1 * retainDays); cal.add(Calendar.DATE, -1 * retainDays); // Add 5 more files more than retainDays old for (int i = 5; i >= 0; i--) { for (int i = 5; i >= 0; i--) { File file = mock(File.class); File file = mock(File.class); when(file.lastModified()).thenReturn(cal.getTimeInMillis() - i); mFileAttrProvider.creationDates.put(file, cal.getTimeInMillis() - i); AtomicFile af = new AtomicFile(file); AtomicFile af = new AtomicFile(file); mDataBase.mHistoryFiles.addLast(af); mDataBase.mHistoryFiles.addLast(af); } } mDataBase.prune(retainDays, 10); for (AtomicFile file : mDataBase.mHistoryFiles) { // back to today; trim everything a day + old assertThat(file.getBaseFile().lastModified() > 0); cal.add(Calendar.DATE, 1 * retainDays); } mDataBase.prune(retainDays, cal.getTimeInMillis()); assertThat(mDataBase.mHistoryFiles).containsExactlyElementsIn(expectedFiles); verify(mAlarmManager, times(6)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any()); } } @Test @Test Loading Loading @@ -181,4 +218,12 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { verify(af2, never()).openRead(); verify(af2, never()).openRead(); } } private class TestFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider { public Map<File, Long> creationDates = new HashMap<>(); @Override public long getCreationTime(File file) { return creationDates.get(file); } } } } Loading
services/core/java/com/android/server/notification/NotificationHistoryDatabase.java +100 −19 Original line number Original line Diff line number Diff line Loading @@ -16,8 +16,15 @@ package com.android.server.notification; package com.android.server.notification; import android.app.AlarmManager; import android.app.NotificationHistory; import android.app.NotificationHistory; import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationHistory.HistoricalNotification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; import android.os.Handler; import android.util.AtomicFile; import android.util.AtomicFile; import android.util.Slog; import android.util.Slog; Loading @@ -33,11 +40,15 @@ import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileReader; import java.io.FileWriter; import java.io.FileWriter; import java.io.IOException; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Arrays; import java.util.Calendar; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.Iterator; import java.util.LinkedList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; /** /** * Provides an interface to write and query for notification history data for a user from a Protocol * Provides an interface to write and query for notification history data for a user from a Protocol Loading @@ -52,32 +63,48 @@ public class NotificationHistoryDatabase { private static final String TAG = "NotiHistoryDatabase"; private static final String TAG = "NotiHistoryDatabase"; private static final boolean DEBUG = NotificationManagerService.DBG; private static final boolean DEBUG = NotificationManagerService.DBG; private static final int HISTORY_RETENTION_DAYS = 2; private static final int HISTORY_RETENTION_DAYS = 2; private static final int HISTORY_RETENTION_MS = 24 * 60 * 60 * 1000; private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20; private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20; private static final String ACTION_HISTORY_DELETION = NotificationHistoryDatabase.class.getSimpleName() + ".CLEANUP"; private static final int REQUEST_CODE_DELETION = 1; private static final String SCHEME_DELETION = "delete"; private static final String EXTRA_KEY = "key"; private final Context mContext; private final AlarmManager mAlarmManager; private final Object mLock = new Object(); private final Object mLock = new Object(); private Handler mFileWriteHandler; private Handler mFileWriteHandler; @VisibleForTesting @VisibleForTesting // List of files holding history information, sorted newest to oldest // List of files holding history information, sorted newest to oldest final LinkedList<AtomicFile> mHistoryFiles; final LinkedList<AtomicFile> mHistoryFiles; private final GregorianCalendar mCal; private final File mHistoryDir; private final File mHistoryDir; private final File mVersionFile; private final File mVersionFile; // Current version of the database files schema // Current version of the database files schema private int mCurrentVersion; private int mCurrentVersion; private final WriteBufferRunnable mWriteBufferRunnable; private final WriteBufferRunnable mWriteBufferRunnable; private final FileAttrProvider mFileAttrProvider; // Object containing posted notifications that have not yet been written to disk // Object containing posted notifications that have not yet been written to disk @VisibleForTesting @VisibleForTesting NotificationHistory mBuffer; NotificationHistory mBuffer; public NotificationHistoryDatabase(File dir) { public NotificationHistoryDatabase(Context context, File dir, FileAttrProvider fileAttrProvider) { mContext = context; mAlarmManager = context.getSystemService(AlarmManager.class); mCurrentVersion = DEFAULT_CURRENT_VERSION; mCurrentVersion = DEFAULT_CURRENT_VERSION; mVersionFile = new File(dir, "version"); mVersionFile = new File(dir, "version"); mHistoryDir = new File(dir, "history"); mHistoryDir = new File(dir, "history"); mHistoryFiles = new LinkedList<>(); mHistoryFiles = new LinkedList<>(); mCal = new GregorianCalendar(); mBuffer = new NotificationHistory(); mBuffer = new NotificationHistory(); mWriteBufferRunnable = new WriteBufferRunnable(); mWriteBufferRunnable = new WriteBufferRunnable(); mFileAttrProvider = fileAttrProvider; IntentFilter deletionFilter = new IntentFilter(ACTION_HISTORY_DELETION); deletionFilter.addDataScheme(SCHEME_DELETION); mContext.registerReceiver(mFileCleaupReceiver, deletionFilter); } } public void init(Handler fileWriteHandler) { public void init(Handler fileWriteHandler) { Loading Loading @@ -105,7 +132,8 @@ public class NotificationHistoryDatabase { } } // Sort with newest files first // Sort with newest files first Arrays.sort(files, (lhs, rhs) -> Long.compare(rhs.lastModified(), lhs.lastModified())); Arrays.sort(files, (lhs, rhs) -> Long.compare(mFileAttrProvider.getCreationTime(rhs), mFileAttrProvider.getCreationTime(lhs))); for (File file : files) { for (File file : files) { mHistoryFiles.addLast(new AtomicFile(file)); mHistoryFiles.addLast(new AtomicFile(file)); Loading Loading @@ -197,29 +225,46 @@ public class NotificationHistoryDatabase { } } /** /** * Remove any files that are too old. * Remove any files that are too old and schedule jobs to clean up the rest */ */ public void prune(final int retentionDays, final long currentTimeMillis) { public void prune(final int retentionDays, final long currentTimeMillis) { synchronized (mLock) { synchronized (mLock) { mCal.setTimeInMillis(currentTimeMillis); GregorianCalendar retentionBoundary = new GregorianCalendar(); mCal.add(Calendar.DATE, -1 * retentionDays); retentionBoundary.setTimeInMillis(currentTimeMillis); retentionBoundary.add(Calendar.DATE, -1 * retentionDays); while (!mHistoryFiles.isEmpty()) { final AtomicFile currentOldestFile = mHistoryFiles.getLast(); for (int i = mHistoryFiles.size() - 1; i >= 0; i--) { final long age = currentTimeMillis final AtomicFile currentOldestFile = mHistoryFiles.get(i); - currentOldestFile.getBaseFile().lastModified(); final long creationTime = if (age > mCal.getTimeInMillis()) { mFileAttrProvider.getCreationTime(currentOldestFile.getBaseFile()); if (creationTime <= retentionBoundary.getTimeInMillis()) { if (DEBUG) { if (DEBUG) { Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName()); Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName()); } } currentOldestFile.delete(); currentOldestFile.delete(); mHistoryFiles.removeLast(); mHistoryFiles.removeLast(); } else { } else { // all remaining files are newer than the cut off // all remaining files are newer than the cut off; schedule jobs to delete return; final long deletionTime = creationTime + (retentionDays * HISTORY_RETENTION_MS); scheduleDeletion(currentOldestFile.getBaseFile(), deletionTime); } } } } } } } void scheduleDeletion(File file, long deletionTime) { if (DEBUG) { Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime); } final PendingIntent pi = PendingIntent.getBroadcast(mContext, REQUEST_CODE_DELETION, new Intent(ACTION_HISTORY_DELETION) .setData(new Uri.Builder().scheme(SCHEME_DELETION) .appendPath(file.getAbsolutePath()).build()) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) .putExtra(EXTRA_KEY, file.getAbsolutePath()), PendingIntent.FLAG_UPDATE_CURRENT); mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, deletionTime, pi); } } private void writeLocked(AtomicFile file, NotificationHistory notifications) private void writeLocked(AtomicFile file, NotificationHistory notifications) Loading @@ -245,6 +290,25 @@ public class NotificationHistoryDatabase { } } } } private final BroadcastReceiver mFileCleaupReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action == null) { return; } if (ACTION_HISTORY_DELETION.equals(action)) { try { final String filePath = intent.getStringExtra(EXTRA_KEY); AtomicFile fileToDelete = new AtomicFile(new File(filePath)); fileToDelete.delete(); } catch (Exception e) { Slog.e(TAG, "Failed to delete notification history file", e); } } } }; private final class WriteBufferRunnable implements Runnable { private final class WriteBufferRunnable implements Runnable { @Override @Override public void run() { public void run() { Loading Loading @@ -277,10 +341,7 @@ public class NotificationHistoryDatabase { // Remove packageName entries from pending history // Remove packageName entries from pending history mBuffer.removeNotificationsFromWrite(mPkg); mBuffer.removeNotificationsFromWrite(mPkg); // Remove packageName entries from files on disk, and rewrite them to disk Iterator<AtomicFile> historyFileItr = mHistoryFiles.iterator(); // Since we sort by modified date, we have to update the files oldest to newest to // maintain the original ordering Iterator<AtomicFile> historyFileItr = mHistoryFiles.descendingIterator(); while (historyFileItr.hasNext()) { while (historyFileItr.hasNext()) { final AtomicFile af = historyFileItr.next(); final AtomicFile af = historyFileItr.next(); try { try { Loading @@ -297,4 +358,24 @@ public class NotificationHistoryDatabase { } } } } } } public static final class NotificationHistoryFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider { final static String TAG = "NotifHistoryFileDate"; public long getCreationTime(File file) { try { BasicFileAttributes attr = Files.readAttributes(FileSystems.getDefault().getPath( file.getAbsolutePath()), BasicFileAttributes.class); return attr.creationTime().to(TimeUnit.MILLISECONDS); } catch (Exception e) { Slog.w(TAG, "Cannot read creation data for file; using file name"); return Long.valueOf(file.getName()); } } } interface FileAttrProvider { long getCreationTime(File file); } } }
services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java +56 −11 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.notification; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.never; Loading @@ -25,7 +26,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationHistory.HistoricalNotification; import android.content.Context; import android.graphics.drawable.Icon; import android.graphics.drawable.Icon; import android.os.Handler; import android.os.Handler; import android.util.AtomicFile; import android.util.AtomicFile; Loading @@ -42,8 +45,17 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations; import java.io.File; import java.io.File; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Calendar; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class) public class NotificationHistoryDatabaseTest extends UiServiceTestCase { public class NotificationHistoryDatabaseTest extends UiServiceTestCase { Loading @@ -51,6 +63,11 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { File mRootDir; File mRootDir; @Mock @Mock Handler mFileWriteHandler; Handler mFileWriteHandler; @Mock Context mContext; @Mock AlarmManager mAlarmManager; TestFileAttrProvider mFileAttrProvider; NotificationHistoryDatabase mDataBase; NotificationHistoryDatabase mDataBase; Loading Loading @@ -85,36 +102,56 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { @Before @Before public void setUp() { public void setUp() { MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this); when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); when(mContext.getUser()).thenReturn(getContext().getUser()); when(mContext.getPackageName()).thenReturn(getContext().getPackageName()); mFileAttrProvider = new TestFileAttrProvider(); mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); mDataBase = new NotificationHistoryDatabase(mRootDir); mDataBase = new NotificationHistoryDatabase(mContext, mRootDir, mFileAttrProvider); mDataBase.init(mFileWriteHandler); mDataBase.init(mFileWriteHandler); } } @Test @Test public void testPrune() { public void testDeletionReceiver() { verify(mContext, times(1)).registerReceiver(any(), any()); } @Test public void testPrune() throws Exception { GregorianCalendar cal = new GregorianCalendar(); cal.setTimeInMillis(10); int retainDays = 1; int retainDays = 1; for (long i = 10; i >= 5; i--) { List<AtomicFile> expectedFiles = new ArrayList<>(); // add 5 files with a creation date of "today" for (long i = cal.getTimeInMillis(); i >= 5; i--) { File file = mock(File.class); File file = mock(File.class); when(file.lastModified()).thenReturn(i); mFileAttrProvider.creationDates.put(file, i); AtomicFile af = new AtomicFile(file); AtomicFile af = new AtomicFile(file); expectedFiles.add(af); mDataBase.mHistoryFiles.addLast(af); mDataBase.mHistoryFiles.addLast(af); } } GregorianCalendar cal = new GregorianCalendar(); cal.setTimeInMillis(5); cal.add(Calendar.DATE, -1 * retainDays); cal.add(Calendar.DATE, -1 * retainDays); // Add 5 more files more than retainDays old for (int i = 5; i >= 0; i--) { for (int i = 5; i >= 0; i--) { File file = mock(File.class); File file = mock(File.class); when(file.lastModified()).thenReturn(cal.getTimeInMillis() - i); mFileAttrProvider.creationDates.put(file, cal.getTimeInMillis() - i); AtomicFile af = new AtomicFile(file); AtomicFile af = new AtomicFile(file); mDataBase.mHistoryFiles.addLast(af); mDataBase.mHistoryFiles.addLast(af); } } mDataBase.prune(retainDays, 10); for (AtomicFile file : mDataBase.mHistoryFiles) { // back to today; trim everything a day + old assertThat(file.getBaseFile().lastModified() > 0); cal.add(Calendar.DATE, 1 * retainDays); } mDataBase.prune(retainDays, cal.getTimeInMillis()); assertThat(mDataBase.mHistoryFiles).containsExactlyElementsIn(expectedFiles); verify(mAlarmManager, times(6)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any()); } } @Test @Test Loading Loading @@ -181,4 +218,12 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { verify(af2, never()).openRead(); verify(af2, never()).openRead(); } } private class TestFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider { public Map<File, Long> creationDates = new HashMap<>(); @Override public long getCreationTime(File file) { return creationDates.get(file); } } } }