From b317ff7fa262f8ec45152a30cdb4e9a151ae7a34 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 26 Nov 2019 14:20:51 -0500 Subject: [PATCH] Hook up notification history to NMS. All of the lifecycle events for the history are in place now with the exception of triggering a write when the power button is long pressed The new tests interacted with DeviceConfig in such a way that callbacks were being received after tests were completed, so I also had to add an unregistration step for the DeviceConfig listener. Test: atest Bug: 137396965 Change-Id: I9ad1197105a5c2434444965a1649485e36e4b692 --- .../android/app/INotificationManager.aidl | 3 + .../NotificationHistoryDatabase.java | 1 + .../NotificationHistoryManager.java | 9 +- .../NotificationManagerService.java | 146 +++++++++++++++--- .../NotificationHistoryDatabaseTest.java | 11 ++ .../NotificationManagerServiceTest.java | 68 +++++++- .../server/notification/RoleObserverTest.java | 5 +- 7 files changed, 214 insertions(+), 29 deletions(-) diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 9aca22360fd81..0957dba4eac15 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -21,6 +21,7 @@ import android.app.ITransientNotification; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.NotificationHistory; import android.app.NotificationManager; import android.content.ComponentName; import android.content.Intent; @@ -119,6 +120,8 @@ interface INotificationManager @UnsupportedAppUsage StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count); + NotificationHistory getNotificationHistory(String callingPkg); + void registerListener(in INotificationListener listener, in ComponentName component, int userid); void unregisterListener(in INotificationListener listener, int userid); diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java index 4ec79e1377604..2247e54ac5f7d 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java @@ -183,6 +183,7 @@ public class NotificationHistoryDatabase { public NotificationHistory readNotificationHistory() { synchronized (mLock) { NotificationHistory notifications = new NotificationHistory(); + notifications.addNotificationsToWrite(mBuffer); for (AtomicFile file : mHistoryFiles) { try { diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java index 2e7016b561b60..1b56c7bb5b8f2 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java @@ -112,7 +112,7 @@ public class NotificationHistoryManager { } } - void onUserRemoved(@UserIdInt int userId) { + public 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 @@ -122,7 +122,7 @@ public class NotificationHistoryManager { } } - void onPackageRemoved(int userId, String packageName) { + public void onPackageRemoved(int userId, String packageName) { synchronized (mLock) { if (!mUserUnlockedStates.get(userId, false)) { if (mHistoryEnabled.get(userId, false)) { @@ -142,7 +142,8 @@ public class NotificationHistoryManager { } } - void triggerWriteToDisk() { + // TODO: wire this up to AMS when power button is long pressed + public void triggerWriteToDisk() { synchronized (mLock) { final int userCount = mUserState.size(); for (int i = 0; i < userCount; i++) { @@ -204,7 +205,7 @@ public class NotificationHistoryManager { } } - public boolean isHistoryEnabled(@UserIdInt int userId) { + boolean isHistoryEnabled(@UserIdInt int userId) { synchronized (mLock) { return mHistoryEnabled.get(userId); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index d5f2d7e550d74..863991cec3bd5 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -103,6 +103,8 @@ import android.Manifest.permission; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.WorkerThread; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; @@ -116,6 +118,8 @@ import android.app.IUriGrantsManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.NotificationHistory; +import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationManager; import android.app.NotificationManager.Policy; import android.app.PendingIntent; @@ -160,6 +164,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.HandlerThread; import android.os.IBinder; import android.os.IDeviceIdleController; @@ -477,12 +482,14 @@ public class NotificationManagerService extends SystemService { private long mLastOverRateLogTime; private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; + private NotificationHistoryManager mHistoryManager; private SnoozeHelper mSnoozeHelper; private GroupHelper mGroupHelper; private int mAutoGroupAtCount; private boolean mIsTelevision; private boolean mIsAutomotive; private boolean mNotificationEffectsEnabledForAutomotive; + private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener; private int mWarnRemoteViewsSizeBytes; private int mStripRemoteViewsSizeBytes; @@ -1547,6 +1554,7 @@ public class NotificationManagerService extends SystemService { mListeners.onUserRemoved(userId); mConditionProviders.onUserRemoved(userId); mAssistants.onUserRemoved(userId); + mHistoryManager.onUserRemoved(userId); handleSavePolicyFile(); } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); @@ -1775,8 +1783,8 @@ public class NotificationManagerService extends SystemService { // TODO: All tests should use this init instead of the one-off setters above. @VisibleForTesting - void init(Looper looper, RankingHandler rankingHandler, IPackageManager packageManager, - PackageManager packageManagerClient, + void init(WorkerHandler handler, RankingHandler rankingHandler, + IPackageManager packageManager, PackageManager packageManagerClient, LightsManager lightsManager, NotificationListeners notificationListeners, NotificationAssistants notificationAssistants, ConditionProviders conditionProviders, ICompanionDeviceManager companionManager, SnoozeHelper snoozeHelper, @@ -1784,7 +1792,8 @@ public class NotificationManagerService extends SystemService { ActivityManager activityManager, GroupHelper groupHelper, IActivityManager am, UsageStatsManagerInternal appUsageStats, DevicePolicyManagerInternal dpm, IUriGrantsManager ugm, UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, - UserManager userManager) { + UserManager userManager, NotificationHistoryManager historyManager) { + mHandler = handler; Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -1810,7 +1819,6 @@ public class NotificationManagerService extends SystemService { mPlatformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); - mHandler = new WorkerHandler(looper); mUiHandler = new Handler(UiThread.get().getLooper()); String[] extractorNames; try { @@ -1869,6 +1877,7 @@ public class NotificationManagerService extends SystemService { extractorNames); mSnoozeHelper = snoozeHelper; mGroupHelper = groupHelper; + mHistoryManager = historyManager; // This is a ManagedServices object that keeps track of the listeners. mListeners = notificationListeners; @@ -1966,7 +1975,9 @@ public class NotificationManagerService extends SystemService { final File systemDir = new File(Environment.getDataDirectory(), "system"); mRankingThread.start(); - init(Looper.myLooper(), new RankingHandlerWorker(mRankingThread.getLooper()), + WorkerHandler handler = new WorkerHandler(Looper.myLooper()); + + init(handler, new RankingHandlerWorker(mRankingThread.getLooper()), AppGlobals.getPackageManager(), getContext().getPackageManager(), getLocalService(LightsManager.class), new NotificationListeners(AppGlobals.getPackageManager()), @@ -1983,7 +1994,8 @@ public class NotificationManagerService extends SystemService { UriGrantsManager.getService(), LocalServices.getService(UriGrantsManagerInternal.class), (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE), - getContext().getSystemService(UserManager.class)); + getContext().getSystemService(UserManager.class), + new NotificationHistoryManager(getContext(), handler)); // register for various Intents IntentFilter filter = new IntentFilter(); @@ -2036,19 +2048,26 @@ public class NotificationManagerService extends SystemService { } private void registerDeviceConfigChange() { + mDeviceConfigChangedListener = properties -> { + if (!DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace())) { + return; + } + if (properties.getKeyset() + .contains(SystemUiDeviceConfigFlags.NAS_DEFAULT_SERVICE)) { + mAssistants.allowAdjustmentType(Adjustment.KEY_IMPORTANCE); + mAssistants.resetDefaultAssistantsIfNecessary(); + } + }; DeviceConfig.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_SYSTEMUI, - getContext().getMainExecutor(), - (properties) -> { - if (!DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace())) { - return; - } - if (properties.getKeyset() - .contains(SystemUiDeviceConfigFlags.NAS_DEFAULT_SERVICE)) { - mAssistants.allowAdjustmentType(Adjustment.KEY_IMPORTANCE); - mAssistants.resetDefaultAssistantsIfNecessary(); - } - }); + new HandlerExecutor(mHandler), + mDeviceConfigChangedListener); + } + + void unregisterDeviceConfigChange() { + if (mDeviceConfigChangedListener != null) { + DeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigChangedListener); + } } private GroupHelper getGroupHelper() { @@ -2134,10 +2153,21 @@ public class NotificationManagerService extends SystemService { mListeners.onBootPhaseAppsCanStart(); mAssistants.onBootPhaseAppsCanStart(); mConditionProviders.onBootPhaseAppsCanStart(); + mHistoryManager.onBootPhaseAppsCanStart(); registerDeviceConfigChange(); } } + @Override + public void onUnlockUser(@NonNull UserInfo userInfo) { + mHandler.post(() -> mHistoryManager.onUserUnlocked(userInfo.id)); + } + + @Override + public void onStopUser(@NonNull UserInfo userInfo) { + mHandler.post(() -> mHistoryManager.onUserStopped(userInfo.id)); + } + @GuardedBy("mNotificationLock") private void updateListenerHintsLocked() { final int hints = calculateHints(); @@ -2449,10 +2479,56 @@ public class NotificationManagerService extends SystemService { mAppUsageStats.reportInterruptiveNotification(r.sbn.getPackageName(), r.getChannel().getId(), getRealUserId(r.sbn.getUserId())); + mHistoryManager.addNotification(new HistoricalNotification.Builder() + .setPackage(r.sbn.getPackageName()) + .setUid(r.sbn.getUid()) + .setChannelId(r.getChannel().getId()) + .setChannelName(r.getChannel().getName().toString()) + .setPostedTimeMs(r.sbn.getPostTime()) + .setTitle(getHistoryTitle(r.getNotification())) + .setText(getHistoryText( + r.sbn.getPackageContext(getContext()), r.getNotification())) + .setIcon(r.getNotification().getSmallIcon()) + .build()); r.setRecordedInterruption(true); } } + private String getHistoryTitle(Notification n) { + CharSequence title = null; + if (n.extras != null) { + title = n.extras.getCharSequence(Notification.EXTRA_TITLE); + } + return title == null? null : String.valueOf(title); + } + + /** + * Returns the appropriate substring for this notification based on the style of notification. + */ + private String getHistoryText(Context appContext, Notification n) { + CharSequence text = null; + if (n.extras != null) { + text = n.extras.getCharSequence(Notification.EXTRA_TEXT); + + Notification.Builder nb = Notification.Builder.recoverBuilder(appContext, n); + + if (nb.getStyle() instanceof Notification.BigTextStyle) { + text = ((Notification.BigTextStyle) nb.getStyle()).getBigText(); + } else if (nb.getStyle() instanceof Notification.MessagingStyle) { + Notification.MessagingStyle ms = (Notification.MessagingStyle) nb.getStyle(); + final List messages = ms.getMessages(); + if (messages != null && messages.size() > 0) { + text = messages.get(messages.size() - 1).getText(); + } + } + + if (TextUtils.isEmpty(text)) { + text = n.extras.getCharSequence(Notification.EXTRA_TEXT); + } + } + return text == null ? null : String.valueOf(text); + } + /** * Report to usage stats that the user interacted with the notification. * @param r notification record @@ -3343,10 +3419,9 @@ public class NotificationManagerService extends SystemService { /** * System-only API for getting a list of recent (cleared, no longer shown) notifications. - * - * Requires ACCESS_NOTIFICATIONS which is signature|system. */ @Override + @RequiresPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) public StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count) { // enforce() will ensure the calling uid has the correct permission getContext().enforceCallingOrSelfPermission( @@ -3366,6 +3441,29 @@ public class NotificationManagerService extends SystemService { return tmp; } + /** + * System-only API for getting a list of historical notifications. May contain multiple days + * of notifications. + */ + @Override + @WorkerThread + @RequiresPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) + public NotificationHistory getNotificationHistory(String callingPkg) { + // enforce() will ensure the calling uid has the correct permission + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_NOTIFICATIONS, + "NotificationManagerService.getNotificationHistory"); + int uid = Binder.getCallingUid(); + + // noteOp will check to make sure the callingPkg matches the uid + if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg) + == AppOpsManager.MODE_ALLOWED) { + IntArray currentUserIds = mUserProfiles.getCurrentProfileIds(); + return mHistoryManager.readNotificationHistory(currentUserIds.toArray()); + } + return new NotificationHistory(); + } + /** * Register a listener binder directly with the notification manager. * @@ -6831,7 +6929,7 @@ public class NotificationManagerService extends SystemService { } } - private void handleOnPackageChanged(boolean removingPackage, int changeUserId, + void handleOnPackageChanged(boolean removingPackage, int changeUserId, String[] pkgList, int[] uidList) { boolean preferencesChanged = removingPackage; mListeners.onPackagesChanged(removingPackage, pkgList, uidList); @@ -6839,6 +6937,14 @@ public class NotificationManagerService extends SystemService { mConditionProviders.onPackagesChanged(removingPackage, pkgList, uidList); preferencesChanged |= mPreferencesHelper.onPackagesChanged( removingPackage, changeUserId, pkgList, uidList); + if (removingPackage) { + int size = Math.min(pkgList.length, uidList.length); + for (int i = 0; i < size; i++) { + final String pkg = pkgList[i]; + final int uid = uidList[i]; + mHistoryManager.onPackageRemoved(UserHandle.getUserId(uid), pkg); + } + } if (preferencesChanged) { handleSavePolicyFile(); } 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 a00afecda0729..9ad6986f2f90e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AlarmManager; +import android.app.NotificationHistory; import android.app.NotificationHistory.HistoricalNotification; import android.content.Context; import android.graphics.drawable.Icon; @@ -198,6 +199,16 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { } } + @Test + public void testReadNotificationHistory_readsBuffer() throws Exception { + HistoricalNotification hn = getHistoricalNotification(1); + mDataBase.addNotification(hn); + + NotificationHistory nh = mDataBase.readNotificationHistory(); + + assertThat(nh.getNotificationsToWrite()).contains(hn); + } + @Test public void testReadNotificationHistory_withNumFilterDoesNotReadExtraFiles() throws Exception { AtomicFile af = mock(AtomicFile.class); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 21de668e10c3a..876e77acde868 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -218,6 +218,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock RankingHandler mRankingHandler; + private static final int MAX_POST_DELAY = 1000; + private NotificationChannel mTestNotificationChannel = new NotificationChannel( TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); @@ -246,6 +248,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mNotificationAssistantAccessGrantedCallback; @Mock UserManager mUm; + @Mock + NotificationHistoryManager mHistoryManager; // Use a Testable subclass so we can simulate calls from the system without failing. private static class TestableNotificationManagerService extends NotificationManagerService { @@ -404,13 +408,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mAssistants.isAdjustmentAllowed(anyString())).thenReturn(true); - mService.init(mTestableLooper.getLooper(), mRankingHandler, - mPackageManager, mPackageManagerClient, mockLightsManager, + mService.init(mService.new WorkerHandler(mTestableLooper.getLooper()), + mRankingHandler, mPackageManager, mPackageManagerClient, mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr, mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAppUsageStats, mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal, - mAppOpsManager, mUm); + mAppOpsManager, mUm, mHistoryManager); mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); mService.setAudioManager(mAudioManager); @@ -430,6 +434,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void tearDown() throws Exception { if (mFile != null) mFile.delete(); clearDeviceConfig(); + mService.unregisterDeviceConfigChange(); InstrumentationRegistry.getInstrumentation() .getUiAutomation().dropShellPermissionIdentity(); } @@ -5947,4 +5952,61 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } // TODO: add tests for the rest of the non-empty cases + + @Test + public void testOnUnlockUser() { + UserInfo ui = new UserInfo(); + ui.id = 10; + mService.onUnlockUser(ui); + waitForIdle(); + + verify(mHistoryManager, timeout(MAX_POST_DELAY).times(1)).onUserUnlocked(ui.id); + } + + @Test + public void testOnStopUser() { + UserInfo ui = new UserInfo(); + ui.id = 10; + mService.onStopUser(ui); + waitForIdle(); + + verify(mHistoryManager, timeout(MAX_POST_DELAY).times(1)).onUserStopped(ui.id); + } + + @Test + public void testOnBootPhase() { + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + + verify(mHistoryManager, never()).onBootPhaseAppsCanStart(); + + mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); + + verify(mHistoryManager, times(1)).onBootPhaseAppsCanStart(); + } + + @Test + public void testHandleOnPackageChanged() { + String[] pkgs = new String[] {PKG, PKG_N_MR1}; + int[] uids = new int[] {mUid, UserHandle.PER_USER_RANGE + 1}; + + mService.handleOnPackageChanged(false, USER_SYSTEM, pkgs, uids); + + verify(mHistoryManager, never()).onPackageRemoved(anyInt(), anyString()); + + mService.handleOnPackageChanged(true, USER_SYSTEM, pkgs, uids); + + verify(mHistoryManager, times(1)).onPackageRemoved(UserHandle.getUserId(uids[0]), pkgs[0]); + verify(mHistoryManager, times(1)).onPackageRemoved(UserHandle.getUserId(uids[1]), pkgs[1]); + } + + @Test + public void testNotificationHistory_addNoisyNotification() throws Exception { + NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, + null /* tvExtender */, false); + mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.sbn.getTag(), + nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); + waitForIdle(); + + verify(mHistoryManager, times(1)).addNotification(any()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java index 7f9f489c509ac..c828f02901b3f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java @@ -129,7 +129,8 @@ public class RoleObserverTest extends UiServiceTestCase { mRoleObserver = mService.new RoleObserver(mRoleManager, mPm, mExecutor); try { - mService.init(mock(Looper.class), mock(RankingHandler.class), + mService.init(mService.new WorkerHandler(mock(Looper.class)), + mock(RankingHandler.class), mock(IPackageManager.class), mock(PackageManager.class), mock(LightsManager.class), mock(NotificationListeners.class), mock(NotificationAssistants.class), @@ -140,7 +141,7 @@ public class RoleObserverTest extends UiServiceTestCase { mock(UsageStatsManagerInternal.class), mock(DevicePolicyManagerInternal.class), mock(IUriGrantsManager.class), mock(UriGrantsManagerInternal.class), - mock(AppOpsManager.class), mUm); + mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class)); } catch (SecurityException e) { if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { throw e;