diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d6fa02c6ff5bd..94a6382227f3c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9513,6 +9513,16 @@ public final class Settings { public static final String LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS = "lock_screen_show_silent_notifications"; + /** + * Indicates whether "seen" notifications should be suppressed from the lockscreen. + *

+ * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS = + "lock_screen_show_only_unseen_notifications"; + /** * Indicates whether snooze options should be shown on notifications *

diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 46a94fdfcd17a..99b15db780dcb 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -165,6 +165,9 @@ true + + false + 1 diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 92a938cb0b47a..1b0b6b4ff796c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -146,6 +146,7 @@ public class SecureSettings { Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, Settings.Secure.SHOW_NOTIFICATION_SNOOZE, Settings.Secure.NOTIFICATION_HISTORY_ENABLED, Settings.Secure.ZEN_DURATION, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index eabf4cc8edfd6..4fa490ffaed54 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -219,6 +219,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index ded7e785ca756..84a5593913434 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3631,7 +3631,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 211; + private static final int SETTINGS_VERSION = 212; private final int mUserId; @@ -5527,6 +5527,26 @@ public class SettingsProvider extends ContentProvider { } currentVersion = 211; } + if (currentVersion == 211) { + // Version 211: Set default value for + // Secure#LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final Setting lockScreenUnseenSetting = secureSettings + .getSettingLocked(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS); + if (lockScreenUnseenSetting.isNull()) { + final boolean defSetting = getContext().getResources() + .getBoolean(R.bool.def_lock_screen_show_only_unseen_notifications); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + defSetting ? "1" : "0", + null /* tag */, + true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } + + currentVersion = 212; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 4133802189a51..76252d0fc5b9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -16,8 +16,13 @@ package com.android.systemui.statusbar.notification.collection.coordinator +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings import androidx.annotation.VisibleForTesting +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState @@ -30,11 +35,20 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxy import javax.inject.Inject import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch /** @@ -45,16 +59,19 @@ import kotlinx.coroutines.launch class KeyguardCoordinator @Inject constructor( + @Background private val bgDispatcher: CoroutineDispatcher, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardRepository: KeyguardRepository, private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, + private val secureSettings: SecureSettings, private val seenNotifsProvider: SeenNotificationsProviderImpl, private val statusBarStateController: StatusBarStateController, ) : Coordinator { private val unseenNotifications = mutableSetOf() + private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { setupInvalidateNotifListCallbacks() @@ -71,6 +88,7 @@ constructor( pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { clearUnseenWhenKeyguardIsDismissed() } + scope.launch { invalidateWhenUnseenSettingChanges() } } private suspend fun clearUnseenWhenKeyguardIsDismissed() { @@ -85,6 +103,36 @@ constructor( } } + private suspend fun invalidateWhenUnseenSettingChanges() { + secureSettings + // emit whenever the setting has changed + .settingChangesForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_ALL, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getBoolForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) + } + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() + // update local field and invalidate if necessary + .collect { setting -> + if (setting != unseenFilterEnabled) { + unseenFilterEnabled = setting + unseenNotifFilter.invalidateList("unseen setting changed") + } + } + } + private val collectionListener = object : NotifCollectionListener { override fun onEntryAdded(entry: NotificationEntry) { @@ -112,6 +160,8 @@ constructor( override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = when { + // Don't apply filter if the setting is disabled + !unseenFilterEnabled -> false // Don't apply filter if the keyguard isn't currently showing !keyguardRepository.isKeyguardShowing() -> false // Don't apply the filter if the notification is unseen @@ -165,3 +215,15 @@ constructor( private val SEEN_TIMEOUT = 5.seconds } } + +private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow = + conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + registerContentObserverForUser(name, observer, userHandle) + awaitClose { unregisterContentObserver(observer) } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt index 5f19fac8666a1..be6b1dc8e4a43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification +import android.os.UserHandle +import android.provider.Settings import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,6 +31,7 @@ import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProvider @@ -38,6 +41,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,6 +51,8 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.same +import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify import java.util.function.Consumer @@ -175,6 +181,42 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } + @Test + fun unseenFilterInvalidatesWhenSettingChanges() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is not showing + keyguardRepository.setKeyguardShowing(false) + runKeyguardCoordinatorTest { + // GIVEN: A notification is present + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // GIVEN: The setting for filtering unseen notifications is disabled + showOnlyUnseenNotifsOnKeyguardSetting = false + + // GIVEN: The pipeline has registered the unseen filter for invalidation + val invalidationListener: Pluggable.PluggableListener = mock() + unseenFilter.setInvalidationListener(invalidationListener) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not filtered out + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + + // WHEN: The secure setting is changed + showOnlyUnseenNotifsOnKeyguardSetting = true + + // THEN: The pipeline is invalidated + verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString()) + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + @Test fun unseenFilterAllowsNewNotif() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) @@ -276,22 +318,32 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private fun runKeyguardCoordinatorTest( testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit ) { - val testScope = TestScope(UnconfinedTestDispatcher()) + val testDispatcher = UnconfinedTestDispatcher() + val testScope = TestScope(testDispatcher) + val fakeSettings = FakeSettings().apply { + putBool(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, true) + } val seenNotificationsProvider = SeenNotificationsProviderImpl() val keyguardCoordinator = KeyguardCoordinator( + testDispatcher, keyguardNotifVisibilityProvider, keyguardRepository, notifPipelineFlags, testScope.backgroundScope, sectionHeaderVisibilityProvider, + fakeSettings, seenNotificationsProvider, statusBarStateController, ) keyguardCoordinator.attach(notifPipeline) testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { - KeyguardCoordinatorTestScope(keyguardCoordinator, testScope, seenNotificationsProvider) - .testBlock() + KeyguardCoordinatorTestScope( + keyguardCoordinator, + testScope, + seenNotificationsProvider, + fakeSettings, + ).testBlock() } } @@ -299,6 +351,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private val keyguardCoordinator: KeyguardCoordinator, private val scope: TestScope, val seenNotificationsProvider: SeenNotificationsProvider, + private val fakeSettings: FakeSettings, ) : CoroutineScope by scope { val testScheduler: TestCoroutineScheduler get() = scope.testScheduler @@ -316,5 +369,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val collectionListener: NotifCollectionListener by lazy { withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) } } + + var showOnlyUnseenNotifsOnKeyguardSetting: Boolean + get() = + fakeSettings.getBoolForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) + set(value) { + fakeSettings.putBoolForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + value, + UserHandle.USER_CURRENT, + ) + } } }