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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20620796

Change-Id: I6e383f9d28993b2d0d48abd2d4b3e130b3f5117d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Steve Elliott
2022-12-15 19:23:20 +00:00
committed by Automerger Merge Worker
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 =
"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
* <p>

View File

@@ -165,6 +165,9 @@
<!-- Default for Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS -->
<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 -->
<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_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,

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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<NotificationEntry>()
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<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
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<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
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,
)
}
}
}