From a614c279f5bfa3002af4678a65477858c15550ea Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 15 Nov 2019 16:43:29 -0500 Subject: [PATCH] Add multiuser support for notification history Since the data is stored in user encrypted storage, we need an intermediate layer that can manage the per user databases Test: atest Bug: 137396965 Change-Id: I66b6f830b3109e0df34d4bb95e32e5c035d0adac --- .../java/android/app/NotificationHistory.java | 8 + .../android/app/NotificationHistoryTest.java | 27 ++ .../NotificationHistoryDatabase.java | 17 +- .../NotificationHistoryDatabaseFactory.java | 41 +++ .../NotificationHistoryManager.java | 245 ++++++++++++++ .../NotificationHistoryDatabaseTest.java | 10 +- .../NotificationHistoryManagerTest.java | 315 ++++++++++++++++++ 7 files changed, 647 insertions(+), 16 deletions(-) create mode 100644 services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java create mode 100644 services/core/java/com/android/server/notification/NotificationHistoryManager.java create mode 100644 services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java index c35246b493955..8ba39a883555a 100644 --- a/core/java/android/app/NotificationHistory.java +++ b/core/java/android/app/NotificationHistory.java @@ -311,6 +311,14 @@ public final class NotificationHistory implements Parcelable { mHistoryCount++; } + public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) { + for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) { + // TODO: consider merging by date + addNotificationToWrite(hn); + } + poolStringsFromNotifications(); + } + /** * Removes a package's historical notifications and regenerates the string pool */ diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java index 08595bb43e06a..f9a6a5c2af4d7 100644 --- a/core/tests/coretests/src/android/app/NotificationHistoryTest.java +++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java @@ -113,6 +113,33 @@ public class NotificationHistoryTest { assertThat(history.getHistoryCount()).isEqualTo(2); } + @Test + public void testAddNotificationsToWrite() { + NotificationHistory history = new NotificationHistory(); + HistoricalNotification n = getHistoricalNotification(0); + HistoricalNotification n2 = getHistoricalNotification(1); + history.addNotificationToWrite(n2); + history.addNotificationToWrite(n); + + NotificationHistory secondHistory = new NotificationHistory(); + HistoricalNotification n3 = getHistoricalNotification(2); + HistoricalNotification n4 = getHistoricalNotification(3); + secondHistory.addNotificationToWrite(n4); + secondHistory.addNotificationToWrite(n3); + + history.addNotificationsToWrite(secondHistory); + + assertThat(history.getNotificationsToWrite().size()).isEqualTo(4); + assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2); + assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n); + assertThat(history.getNotificationsToWrite().get(2)).isSameAs(n4); + assertThat(history.getNotificationsToWrite().get(3)).isSameAs(n3); + assertThat(history.getHistoryCount()).isEqualTo(4); + + assertThat(history.getPooledStringsToWrite()).asList().contains(n2.getChannelName()); + assertThat(history.getPooledStringsToWrite()).asList().contains(n4.getPackage()); + } + @Test public void testPoolStringsFromNotifications() { NotificationHistory history = new NotificationHistory(); diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java index f05b2bf9711ff..378ca4a51974d 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java @@ -75,7 +75,7 @@ public class NotificationHistoryDatabase { private final Context mContext; private final AlarmManager mAlarmManager; private final Object mLock = new Object(); - private Handler mFileWriteHandler; + private final Handler mFileWriteHandler; @VisibleForTesting // List of files holding history information, sorted newest to oldest final LinkedList mHistoryFiles; @@ -90,11 +90,12 @@ public class NotificationHistoryDatabase { @VisibleForTesting NotificationHistory mBuffer; - public NotificationHistoryDatabase(Context context, File dir, + public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir, FileAttrProvider fileAttrProvider) { mContext = context; mAlarmManager = context.getSystemService(AlarmManager.class); mCurrentVersion = DEFAULT_CURRENT_VERSION; + mFileWriteHandler = fileWriteHandler; mVersionFile = new File(dir, "version"); mHistoryDir = new File(dir, "history"); mHistoryFiles = new LinkedList<>(); @@ -107,10 +108,8 @@ public class NotificationHistoryDatabase { mContext.registerReceiver(mFileCleaupReceiver, deletionFilter); } - public void init(Handler fileWriteHandler) { + public void init() { synchronized (mLock) { - mFileWriteHandler = fileWriteHandler; - try { mHistoryDir.mkdir(); mVersionFile.createNewFile(); @@ -160,13 +159,13 @@ public class NotificationHistoryDatabase { } } - void forceWriteToDisk() { + public void forceWriteToDisk() { if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) { mFileWriteHandler.post(mWriteBufferRunnable); } } - void onPackageRemoved(String packageName) { + public void onPackageRemoved(String packageName) { RemovePackageRunnable rpr = new RemovePackageRunnable(packageName); mFileWriteHandler.post(rpr); } @@ -227,7 +226,7 @@ public class NotificationHistoryDatabase { /** * Remove any files that are too old and schedule jobs to clean up the rest */ - public void prune(final int retentionDays, final long currentTimeMillis) { + void prune(final int retentionDays, final long currentTimeMillis) { synchronized (mLock) { GregorianCalendar retentionBoundary = new GregorianCalendar(); retentionBoundary.setTimeInMillis(currentTimeMillis); @@ -252,7 +251,7 @@ public class NotificationHistoryDatabase { } } - void scheduleDeletion(File file, long deletionTime) { + private void scheduleDeletion(File file, long deletionTime) { if (DEBUG) { Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime); } diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java new file mode 100644 index 0000000000000..b4940a5ab6475 --- /dev/null +++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.Handler; + +import java.io.File; + +public class NotificationHistoryDatabaseFactory { + + private static NotificationHistoryDatabase sTestingNotificationHistoryDb; + + public static void setTestingNotificationHistoryDatabase(NotificationHistoryDatabase db) { + sTestingNotificationHistoryDb = db; + } + + public static NotificationHistoryDatabase create(@NonNull Context context, + @NonNull Handler handler, @NonNull File rootDir, + @NonNull NotificationHistoryDatabase.FileAttrProvider fileAttrProvider) { + if(sTestingNotificationHistoryDb != null) { + return sTestingNotificationHistoryDb; + } + return new NotificationHistoryDatabase(context, handler, rootDir, fileAttrProvider); + } +} diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java new file mode 100644 index 0000000000000..a350a6b2acd58 --- /dev/null +++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.NotificationHistory; +import android.app.NotificationHistory.HistoricalNotification; +import android.content.Context; +import android.os.Environment; +import android.os.UserManager; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.IoThread; +import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Keeps track of per-user notification histories. + */ +public class NotificationHistoryManager { + private static final String TAG = "NotificationHistory"; + private static final boolean DEBUG = NotificationManagerService.DBG; + + @VisibleForTesting + static final String DIRECTORY_PER_USER = "notification_history"; + + private final Context mContext; + private final UserManager mUserManager; + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final SparseArray mUserState = new SparseArray<>(); + @GuardedBy("mLock") + private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray(); + // TODO: does this need to be persisted across reboots? + @GuardedBy("mLock") + private final SparseArray> mUserPendingPackageRemovals = new SparseArray<>(); + + public NotificationHistoryManager(Context context) { + mContext = context; + mUserManager = context.getSystemService(UserManager.class); + } + + public void onUserUnlocked(@UserIdInt int userId) { + synchronized (mLock) { + mUserUnlockedStates.put(userId, true); + final NotificationHistoryDatabase userHistory = + getUserHistoryAndInitializeIfNeededLocked(userId); + if (userHistory == null) { + Slog.i(TAG, "Attempted to unlock stopped or removed user " + userId); + return; + } + + // remove any packages that were deleted while the user was locked + final List pendingPackageRemovals = mUserPendingPackageRemovals.get(userId); + if (pendingPackageRemovals != null) { + for (int i = 0; i < pendingPackageRemovals.size(); i++) { + userHistory.onPackageRemoved(pendingPackageRemovals.get(i)); + } + mUserPendingPackageRemovals.put(userId, null); + } + } + } + + public void onUserStopped(@UserIdInt int userId) { + synchronized (mLock) { + mUserUnlockedStates.put(userId, false); + mUserState.put(userId, null); // release the service (mainly for GC) + } + } + + void onUserRemoved(@UserIdInt int userId) { + synchronized (mLock) { + // Actual data deletion is handled by other parts of the system (the entire directory is + // removed) - we just need clean up our internal state for GC + mUserPendingPackageRemovals.put(userId, null); + onUserStopped(userId); + } + } + + void onPackageRemoved(int userId, String packageName) { + synchronized (mLock) { + if (!mUserUnlockedStates.get(userId, false)) { + List userPendingRemovals = + mUserPendingPackageRemovals.get(userId, new ArrayList<>()); + userPendingRemovals.add(packageName); + mUserPendingPackageRemovals.put(userId, userPendingRemovals); + return; + } + final NotificationHistoryDatabase userHistory = mUserState.get(userId); + if (userHistory == null) { + return; + } + + userHistory.onPackageRemoved(packageName); + } + } + + void triggerWriteToDisk() { + synchronized (mLock) { + final int userCount = mUserState.size(); + for (int i = 0; i < userCount; i++) { + final int userId = mUserState.keyAt(i); + if (!mUserUnlockedStates.get(userId)) { + continue; + } + NotificationHistoryDatabase userHistory = mUserState.get(userId); + if (userHistory != null) { + userHistory.forceWriteToDisk(); + } + } + } + } + + public void addNotification(@NonNull final HistoricalNotification notification) { + synchronized (mLock) { + final NotificationHistoryDatabase userHistory = + getUserHistoryAndInitializeIfNeededLocked(notification.getUserId()); + if (userHistory == null) { + Slog.w(TAG, "Attempted to add notif for locked/gone user " + + notification.getUserId()); + return; + } + userHistory.addNotification(notification); + } + } + + public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) { + synchronized (mLock) { + NotificationHistory mergedHistory = new NotificationHistory(); + if (userIds == null) { + return mergedHistory; + } + for (int userId : userIds) { + final NotificationHistoryDatabase userHistory = + getUserHistoryAndInitializeIfNeededLocked(userId); + if (userHistory == null) { + Slog.i(TAG, "Attempted to read history for locked/gone user " +userId); + continue; + } + mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory()); + } + return mergedHistory; + } + } + + public @NonNull android.app.NotificationHistory readFilteredNotificationHistory( + @UserIdInt int userId, String packageName, String channelId, int maxNotifications) { + synchronized (mLock) { + final NotificationHistoryDatabase userHistory = + getUserHistoryAndInitializeIfNeededLocked(userId); + if (userHistory == null) { + Slog.i(TAG, "Attempted to read history for locked/gone user " +userId); + return new android.app.NotificationHistory(); + } + + return userHistory.readNotificationHistory(packageName, channelId, maxNotifications); + } + } + + @GuardedBy("mLock") + private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked( + int userId) { + NotificationHistoryDatabase userHistory = mUserState.get(userId); + if (userHistory == null) { + final File historyDir = new File(Environment.getDataSystemCeDirectory(userId), + DIRECTORY_PER_USER); + userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(), + historyDir, new NotificationHistoryFileAttrProvider()); + if (mUserUnlockedStates.get(userId)) { + try { + userHistory.init(); + } catch (Exception e) { + if (mUserManager.isUserUnlocked(userId)) { + throw e; // rethrow exception - user is unlocked + } else { + Slog.w(TAG, "Attempted to initialize service for " + + "stopped or removed user " + userId); + return null; + } + } + } else { + // locked! data unavailable + Slog.w(TAG, "Attempted to initialize service for " + + "stopped or removed user " + userId); + return null; + } + mUserState.put(userId, userHistory); + } + return userHistory; + } + + @VisibleForTesting + boolean isUserUnlocked(@UserIdInt int userId) { + synchronized (mLock) { + return mUserUnlockedStates.get(userId); + } + } + + @VisibleForTesting + boolean doesHistoryExistForUser(@UserIdInt int userId) { + synchronized (mLock) { + return mUserState.get(userId) != null; + } + } + + @VisibleForTesting + void replaceNotificationHistoryDatabase(@UserIdInt int userId, + NotificationHistoryDatabase replacement) { + synchronized (mLock) { + if (mUserState.get(userId) != null) { + mUserState.put(userId, replacement); + } + } + } + + @VisibleForTesting + List getPendingPackageRemovalsForUser(@UserIdInt int userId) { + synchronized (mLock) { + return mUserPendingPackageRemovals.get(userId); + } + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java index 608625f9fd10c..a00afecda0729 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java @@ -45,11 +45,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; 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.GregorianCalendar; @@ -109,8 +104,9 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { mFileAttrProvider = new TestFileAttrProvider(); mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); - mDataBase = new NotificationHistoryDatabase(mContext, mRootDir, mFileAttrProvider); - mDataBase.init(mFileWriteHandler); + mDataBase = new NotificationHistoryDatabase( + mContext, mFileWriteHandler, mRootDir, mFileAttrProvider); + mDataBase.init(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java new file mode 100644 index 0000000000000..aa3c4659c413e --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.notification; + +import static android.os.UserHandle.USER_ALL; +import static android.os.UserHandle.USER_SYSTEM; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationHistory; +import android.app.NotificationHistory.HistoricalNotification; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.os.UserManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public class NotificationHistoryManagerTest extends UiServiceTestCase { + + @Mock + Context mContext; + @Mock + UserManager mUserManager; + @Mock + NotificationHistoryDatabase mDb; + + NotificationHistoryManager mHistoryManager; + + private HistoricalNotification getHistoricalNotification(int index) { + return getHistoricalNotification("package" + index, index); + } + + private HistoricalNotification getHistoricalNotification(String packageName, int index) { + String expectedChannelName = "channelName" + index; + String expectedChannelId = "channelId" + index; + int expectedUid = 1123456 + index; + int expectedUserId = index; + long expectedPostTime = 987654321 + index; + String expectedTitle = "title" + index; + String expectedText = "text" + index; + Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(), + index); + + return new HistoricalNotification.Builder() + .setPackage(packageName) + .setChannelName(expectedChannelName) + .setChannelId(expectedChannelId) + .setUid(expectedUid) + .setUserId(expectedUserId) + .setPostedTimeMs(expectedPostTime) + .setTitle(expectedTitle) + .setText(expectedText) + .setIcon(expectedIcon) + .build(); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + when(mContext.getUser()).thenReturn(getContext().getUser()); + when(mContext.getPackageName()).thenReturn(getContext().getPackageName()); + + NotificationHistoryDatabaseFactory.setTestingNotificationHistoryDatabase(mDb); + + mHistoryManager = new NotificationHistoryManager(mContext); + } + + @Test + public void testOnUserUnlocked() { + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse(); + mHistoryManager.onUserUnlocked(USER_SYSTEM); + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue(); + verify(mDb, times(1)).init(); + } + + @Test + public void testOnUserUnlocked_cleansUpRemovedPackages() { + String pkg = "pkg"; + mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg); + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue(); + + verify(mDb, times(1)).onPackageRemoved(pkg); + } + + @Test + public void testOnUserStopped_userExists() { + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.onUserStopped(USER_SYSTEM); + + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse(); + } + + @Test + public void testOnUserStopped_userDoesNotExist() { + mHistoryManager.onUserStopped(USER_SYSTEM); + // no crash + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse(); + } + + @Test + public void testOnUserRemoved_userExists() { + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.onUserRemoved(USER_SYSTEM); + + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse(); + } + + @Test + public void testOnUserRemoved_userDoesNotExist() { + mHistoryManager.onUserRemoved(USER_SYSTEM); + // no crash + assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse(); + assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse(); + } + + @Test + public void testOnUserRemoved_cleanupPendingPackages() { + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.onUserStopped(USER_SYSTEM); + String pkg = "pkg"; + mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg); + mHistoryManager.onUserRemoved(USER_SYSTEM); + + assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).isNull(); + } + + @Test + public void testOnPackageRemoved_userUnlocked() { + String pkg = "pkg"; + NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory); + + mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg); + + verify(userHistory, times(1)).onPackageRemoved(pkg); + } + + @Test + public void testOnPackageRemoved_userLocked() { + String pkg = "pkg"; + mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg); + + assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).contains(pkg); + } + + @Test + public void testOnPackageRemoved_multiUser() { + String pkg = "pkg"; + NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class); + NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem); + + mHistoryManager.onUserUnlocked(USER_ALL); + mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll); + + mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg); + + verify(userHistorySystem, times(1)).onPackageRemoved(pkg); + verify(userHistoryAll, never()).onPackageRemoved(pkg); + } + + @Test + public void testTriggerWriteToDisk() { + NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class); + NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem); + + mHistoryManager.onUserUnlocked(USER_ALL); + mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll); + + mHistoryManager.triggerWriteToDisk(); + + verify(userHistorySystem, times(1)).forceWriteToDisk(); + verify(userHistoryAll, times(1)).forceWriteToDisk(); + } + + @Test + public void testTriggerWriteToDisk_onlyUnlockedUsers() { + NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class); + NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem); + + mHistoryManager.onUserUnlocked(USER_ALL); + mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll); + mHistoryManager.onUserStopped(USER_ALL); + + mHistoryManager.triggerWriteToDisk(); + + verify(userHistorySystem, times(1)).forceWriteToDisk(); + verify(userHistoryAll, never()).forceWriteToDisk(); + } + + @Test + public void testAddNotification_userLocked_noCrash() { + HistoricalNotification hn = getHistoricalNotification("pkg", 1); + + mHistoryManager.addNotification(hn); + } + + @Test + public void testAddNotification() { + HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM); + HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL); + + NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class); + NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem); + + mHistoryManager.onUserUnlocked(USER_ALL); + mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll); + + mHistoryManager.addNotification(hnSystem); + mHistoryManager.addNotification(hnAll); + + verify(userHistorySystem, times(1)).addNotification(hnSystem); + verify(userHistoryAll, times(1)).addNotification(hnAll); + } + + @Test + public void testReadNotificationHistory() { + HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM); + HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL); + + NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class); + NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem); + NotificationHistory nhSystem = mock(NotificationHistory.class); + ArrayList nhSystemList = new ArrayList<>(); + nhSystemList.add(hnSystem); + when(nhSystem.getNotificationsToWrite()).thenReturn(nhSystemList); + when(userHistorySystem.readNotificationHistory()).thenReturn(nhSystem); + + mHistoryManager.onUserUnlocked(USER_ALL); + mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll); + NotificationHistory nhAll = mock(NotificationHistory.class); + ArrayList nhAllList = new ArrayList<>(); + nhAllList.add(hnAll); + when(nhAll.getNotificationsToWrite()).thenReturn(nhAllList); + when(userHistoryAll.readNotificationHistory()).thenReturn(nhAll); + + // ensure read history returns both historical notifs + NotificationHistory nh = mHistoryManager.readNotificationHistory( + new int[] {USER_SYSTEM, USER_ALL}); + assertThat(nh.getNotificationsToWrite()).contains(hnSystem); + assertThat(nh.getNotificationsToWrite()).contains(hnAll); + } + + @Test + public void readFilteredNotificationHistory_userUnlocked() { + NotificationHistory nh = + mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "", "", 1000); + assertThat(nh.getNotificationsToWrite()).isEmpty(); + } + + @Test + public void readFilteredNotificationHistory() { + mHistoryManager.onUserUnlocked(USER_SYSTEM); + + mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "pkg", "chn", 1000); + verify(mDb, times(1)).readNotificationHistory("pkg", "chn", 1000); + } +}