Merge "Secure setting to control seen notif filter" into tm-qpr-dev

This commit is contained in:
Steve Elliott
2022-12-15 18:55:38 +00:00
committed by Android (Google) Code Review
7 changed files with 168 additions and 4 deletions

View File

@@ -9513,6 +9513,16 @@ public final class Settings {
public static final String LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS = public static final String LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS =
"lock_screen_show_silent_notifications"; "lock_screen_show_silent_notifications";
/**
* Indicates whether "seen" notifications should be suppressed from the lockscreen.
* <p>
* 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 * Indicates whether snooze options should be shown on notifications
* <p> * <p>

View File

@@ -165,6 +165,9 @@
<!-- Default for Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS --> <!-- Default for Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS -->
<bool name="def_lock_screen_allow_private_notifications">true</bool> <bool name="def_lock_screen_allow_private_notifications">true</bool>
<!-- Default for Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS -->
<bool name="def_lock_screen_show_only_unseen_notifications">false</bool>
<!-- Default for Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1==on --> <!-- Default for Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1==on -->
<integer name="def_heads_up_enabled">1</integer> <integer name="def_heads_up_enabled">1</integer>

View File

@@ -146,6 +146,7 @@ public class SecureSettings {
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
Settings.Secure.SHOW_NOTIFICATION_SNOOZE, Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
Settings.Secure.NOTIFICATION_HISTORY_ENABLED, Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
Settings.Secure.ZEN_DURATION, Settings.Secure.ZEN_DURATION,

View File

@@ -219,6 +219,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR); 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_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_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.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);

View File

@@ -3631,7 +3631,7 @@ public class SettingsProvider extends ContentProvider {
} }
private final class UpgradeController { private final class UpgradeController {
private static final int SETTINGS_VERSION = 211; private static final int SETTINGS_VERSION = 212;
private final int mUserId; private final int mUserId;
@@ -5527,6 +5527,26 @@ public class SettingsProvider extends ContentProvider {
} }
currentVersion = 211; 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. // vXXX: Add new settings above this point.
if (currentVersion != newVersion) { if (currentVersion != newVersion) {

View File

@@ -16,8 +16,13 @@
package com.android.systemui.statusbar.notification.collection.coordinator 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 androidx.annotation.VisibleForTesting
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application 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.keyguard.data.repository.KeyguardRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState 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.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider 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 javax.inject.Inject
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest 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 import kotlinx.coroutines.launch
/** /**
@@ -45,16 +59,19 @@ import kotlinx.coroutines.launch
class KeyguardCoordinator class KeyguardCoordinator
@Inject @Inject
constructor( constructor(
@Background private val bgDispatcher: CoroutineDispatcher,
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
private val keyguardRepository: KeyguardRepository, private val keyguardRepository: KeyguardRepository,
private val notifPipelineFlags: NotifPipelineFlags, private val notifPipelineFlags: NotifPipelineFlags,
@Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope,
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
private val secureSettings: SecureSettings,
private val seenNotifsProvider: SeenNotificationsProviderImpl, private val seenNotifsProvider: SeenNotificationsProviderImpl,
private val statusBarStateController: StatusBarStateController, private val statusBarStateController: StatusBarStateController,
) : Coordinator { ) : Coordinator {
private val unseenNotifications = mutableSetOf<NotificationEntry>() private val unseenNotifications = mutableSetOf<NotificationEntry>()
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) { override fun attach(pipeline: NotifPipeline) {
setupInvalidateNotifListCallbacks() setupInvalidateNotifListCallbacks()
@@ -71,6 +88,7 @@ constructor(
pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener) pipeline.addCollectionListener(collectionListener)
scope.launch { clearUnseenWhenKeyguardIsDismissed() } scope.launch { clearUnseenWhenKeyguardIsDismissed() }
scope.launch { invalidateWhenUnseenSettingChanges() }
} }
private suspend fun clearUnseenWhenKeyguardIsDismissed() { 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 = private val collectionListener =
object : NotifCollectionListener { object : NotifCollectionListener {
override fun onEntryAdded(entry: NotificationEntry) { override fun onEntryAdded(entry: NotificationEntry) {
@@ -112,6 +160,8 @@ constructor(
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
when { when {
// Don't apply filter if the setting is disabled
!unseenFilterEnabled -> false
// Don't apply filter if the keyguard isn't currently showing // Don't apply filter if the keyguard isn't currently showing
!keyguardRepository.isKeyguardShowing() -> false !keyguardRepository.isKeyguardShowing() -> false
// Don't apply the filter if the notification is unseen // Don't apply the filter if the notification is unseen
@@ -165,3 +215,15 @@ constructor(
private val SEEN_TIMEOUT = 5.seconds private val SEEN_TIMEOUT = 5.seconds
} }
} }
private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow<Unit> =
conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
registerContentObserverForUser(name, observer, userHandle)
awaitClose { unregisterContentObserver(observer) }
}

View File

@@ -18,6 +18,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator package com.android.systemui.statusbar.notification.collection.coordinator
import android.app.Notification import android.app.Notification
import android.os.UserHandle
import android.provider.Settings
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder 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.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.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProvider 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.eq
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -47,6 +51,8 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.same
import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import java.util.function.Consumer 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<NotifFilter> = 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 @Test
fun unseenFilterAllowsNewNotif() { fun unseenFilterAllowsNewNotif() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
@@ -276,22 +318,32 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
private fun runKeyguardCoordinatorTest( private fun runKeyguardCoordinatorTest(
testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit 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 seenNotificationsProvider = SeenNotificationsProviderImpl()
val keyguardCoordinator = val keyguardCoordinator =
KeyguardCoordinator( KeyguardCoordinator(
testDispatcher,
keyguardNotifVisibilityProvider, keyguardNotifVisibilityProvider,
keyguardRepository, keyguardRepository,
notifPipelineFlags, notifPipelineFlags,
testScope.backgroundScope, testScope.backgroundScope,
sectionHeaderVisibilityProvider, sectionHeaderVisibilityProvider,
fakeSettings,
seenNotificationsProvider, seenNotificationsProvider,
statusBarStateController, statusBarStateController,
) )
keyguardCoordinator.attach(notifPipeline) keyguardCoordinator.attach(notifPipeline)
testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
KeyguardCoordinatorTestScope(keyguardCoordinator, testScope, seenNotificationsProvider) KeyguardCoordinatorTestScope(
.testBlock() keyguardCoordinator,
testScope,
seenNotificationsProvider,
fakeSettings,
).testBlock()
} }
} }
@@ -299,6 +351,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
private val keyguardCoordinator: KeyguardCoordinator, private val keyguardCoordinator: KeyguardCoordinator,
private val scope: TestScope, private val scope: TestScope,
val seenNotificationsProvider: SeenNotificationsProvider, val seenNotificationsProvider: SeenNotificationsProvider,
private val fakeSettings: FakeSettings,
) : CoroutineScope by scope { ) : CoroutineScope by scope {
val testScheduler: TestCoroutineScheduler val testScheduler: TestCoroutineScheduler
get() = scope.testScheduler get() = scope.testScheduler
@@ -316,5 +369,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
val collectionListener: NotifCollectionListener by lazy { val collectionListener: NotifCollectionListener by lazy {
withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) } 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,
)
}
} }
} }