Merge changes from topic "tangor-keyguard-notifs" into tm-qpr-dev

* changes:
  Filter out seen notifications from keyguard
  Convert KeyguardCoordinator to Kotlin
This commit is contained in:
Steve Elliott
2022-11-16 17:20:27 +00:00
committed by Android (Google) Code Review
5 changed files with 341 additions and 113 deletions

View File

@@ -90,7 +90,8 @@ object Flags {
// TODO(b/257315550): Tracking Bug
val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when")
// next id: 119
val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
// 200 - keyguard/lockscreen
// ** Flag retired **

View File

@@ -40,4 +40,8 @@ class NotifPipelineFlags @Inject constructor(
val isSemiStableSortEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
}
val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy {
featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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.systemui.statusbar.notification.collection.coordinator;
import androidx.annotation.NonNull;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import javax.inject.Inject;
/**
* Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
* headers on the lockscreen.
*/
@CoordinatorScope
public class KeyguardCoordinator implements Coordinator {
private static final String TAG = "KeyguardCoordinator";
private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
private final StatusBarStateController mStatusBarStateController;
@Inject
public KeyguardCoordinator(
KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
StatusBarStateController statusBarStateController) {
mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
mStatusBarStateController = statusBarStateController;
}
@Override
public void attach(NotifPipeline pipeline) {
setupInvalidateNotifListCallbacks();
// Filter at the "finalize" stage so that views remain bound by PreparationCoordinator
pipeline.addFinalizeFilter(mNotifFilter);
mKeyguardNotificationVisibilityProvider
.addOnStateChangedListener(this::invalidateListFromFilter);
updateSectionHeadersVisibility();
}
private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
@Override
public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry);
}
};
// TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
// these same updates
private void setupInvalidateNotifListCallbacks() {
}
private void invalidateListFromFilter(String reason) {
updateSectionHeadersVisibility();
mNotifFilter.invalidateList(reason);
}
private void updateSectionHeadersVisibility() {
boolean onKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
boolean neverShowSections = mSectionHeaderVisibilityProvider.getNeverShowSectionHeaders();
boolean showSections = !onKeyguard && !neverShowSections;
mSectionHeaderVisibilityProvider.setSectionHeadersVisible(showSections);
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.notification.collection.coordinator
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
* headers on the lockscreen.
*/
@CoordinatorScope
class KeyguardCoordinator
@Inject
constructor(
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
private val keyguardRepository: KeyguardRepository,
private val notifPipelineFlags: NotifPipelineFlags,
@Application private val scope: CoroutineScope,
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
private val statusBarStateController: StatusBarStateController,
) : Coordinator {
private val unseenNotifications = mutableSetOf<NotificationEntry>()
override fun attach(pipeline: NotifPipeline) {
setupInvalidateNotifListCallbacks()
// Filter at the "finalize" stage so that views remain bound by PreparationCoordinator
pipeline.addFinalizeFilter(notifFilter)
keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter)
updateSectionHeadersVisibility()
if (notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard) {
attachUnseenFilter(pipeline)
}
}
private fun attachUnseenFilter(pipeline: NotifPipeline) {
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { clearUnseenWhenKeyguardIsDismissed() }
}
private suspend fun clearUnseenWhenKeyguardIsDismissed() {
// Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes
// during the timeout period
keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing ->
if (!isKeyguardShowing) {
unseenNotifFilter.invalidateList("keyguard no longer showing")
delay(SEEN_TIMEOUT)
unseenNotifications.clear()
}
}
}
private val collectionListener =
object : NotifCollectionListener {
override fun onEntryAdded(entry: NotificationEntry) {
if (keyguardRepository.isKeyguardShowing()) {
unseenNotifications.add(entry)
}
}
override fun onEntryUpdated(entry: NotificationEntry) {
if (keyguardRepository.isKeyguardShowing()) {
unseenNotifications.add(entry)
}
}
override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
unseenNotifications.remove(entry)
}
}
@VisibleForTesting
internal val unseenNotifFilter =
object : NotifFilter("$TAG-unseen") {
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
when {
// Don't apply filter if the keyguard isn't currently showing
!keyguardRepository.isKeyguardShowing() -> false
// Don't apply the filter if the notification is unseen
unseenNotifications.contains(entry) -> false
// Don't apply the filter to (non-promoted) group summaries
// - summary will be pruned if necessary, depending on if children are filtered
entry.parent?.summary == entry -> false
else -> true
}
}
private val notifFilter: NotifFilter =
object : NotifFilter(TAG) {
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
}
// TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
// these same updates
private fun setupInvalidateNotifListCallbacks() {}
private fun invalidateListFromFilter(reason: String) {
updateSectionHeadersVisibility()
notifFilter.invalidateList(reason)
}
private fun updateSectionHeadersVisibility() {
val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
val neverShowSections = sectionHeaderVisibilityProvider.neverShowSectionHeaders
val showSections = !onKeyguard && !neverShowSections
sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections
}
companion object {
private const val TAG = "KeyguardCoordinator"
private val SEEN_TIMEOUT = 5.seconds
}
}

View File

@@ -13,57 +13,55 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.statusbar.notification.collection.coordinator
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.NotifPipelineFlags
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.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import java.util.function.Consumer
import org.junit.Before
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import java.util.function.Consumer
import kotlin.time.Duration.Companion.seconds
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
class KeyguardCoordinatorTest : SysuiTestCase() {
private val notifPipeline: NotifPipeline = mock()
private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
private val keyguardRepository = FakeKeyguardRepository()
private val notifPipelineFlags: NotifPipelineFlags = mock()
private val notifPipeline: NotifPipeline = mock()
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
private val statusBarStateController: StatusBarStateController = mock()
private lateinit var onStateChangeListener: Consumer<String>
private lateinit var keyguardFilter: NotifFilter
@Before
fun setup() {
val keyguardCoordinator = KeyguardCoordinator(
keyguardNotifVisibilityProvider,
sectionHeaderVisibilityProvider,
statusBarStateController
)
keyguardCoordinator.attach(notifPipeline)
onStateChangeListener = withArgCaptor {
verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
}
keyguardFilter = withArgCaptor {
verify(notifPipeline).addFinalizeFilter(capture())
}
}
@Test
fun testSetSectionHeadersVisibleInShade() {
fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
clearInvocations(sectionHeaderVisibilityProvider)
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
onStateChangeListener.accept("state change")
@@ -71,10 +69,176 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
}
@Test
fun testSetSectionHeadersNotVisibleOnKeyguard() {
fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest {
clearInvocations(sectionHeaderVisibilityProvider)
whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
onStateChangeListener.accept("state change")
verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
}
@Test
fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
// GIVEN: Keyguard is not showing, and a notification is present
keyguardRepository.setKeyguardShowing(false)
runKeyguardCoordinatorTest {
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// WHEN: The keyguard is now showing
keyguardRepository.setKeyguardShowing(true)
testScheduler.runCurrent()
// THEN: The notification is recognized as "seen" and is filtered out.
assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
// WHEN: The keyguard goes away
keyguardRepository.setKeyguardShowing(false)
testScheduler.runCurrent()
// THEN: The notification is shown regardless
assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
}
}
@Test
fun unseenFilterAllowsNewNotif() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
// GIVEN: Keyguard is showing, no notifications present
keyguardRepository.setKeyguardShowing(true)
runKeyguardCoordinatorTest {
// WHEN: A new notification is posted
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// THEN: The notification is recognized as "unseen" and is not filtered out.
assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
}
}
@Test
fun unseenFilterSeenGroupSummaryWithUnseenChild() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
// GIVEN: Keyguard is not showing, and a notification is present
keyguardRepository.setKeyguardShowing(false)
runKeyguardCoordinatorTest {
// WHEN: A new notification is posted
val fakeSummary = NotificationEntryBuilder().build()
val fakeChild = NotificationEntryBuilder()
.setGroup(context, "group")
.setGroupSummary(context, false)
.build()
GroupEntryBuilder()
.setSummary(fakeSummary)
.addChild(fakeChild)
.build()
collectionListener.onEntryAdded(fakeSummary)
collectionListener.onEntryAdded(fakeChild)
// WHEN: Keyguard is now showing, both notifications are marked as seen
keyguardRepository.setKeyguardShowing(true)
testScheduler.runCurrent()
// WHEN: The child notification is now unseen
collectionListener.onEntryUpdated(fakeChild)
// THEN: The summary is not filtered out, because the child is unseen
assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
}
}
@Test
fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
// GIVEN: Keyguard is showing, unseen notification is present
keyguardRepository.setKeyguardShowing(true)
runKeyguardCoordinatorTest {
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// WHEN: Keyguard is no longer showing for 5 seconds
keyguardRepository.setKeyguardShowing(false)
testScheduler.runCurrent()
testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
testScheduler.runCurrent()
// THEN: The notification is now recognized as "seen" and is filtered out.
assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
}
}
@Test
fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
// GIVEN: Keyguard is showing, unseen notification is present
keyguardRepository.setKeyguardShowing(true)
runKeyguardCoordinatorTest {
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// WHEN: Keyguard is no longer showing for <5 seconds
keyguardRepository.setKeyguardShowing(false)
testScheduler.runCurrent()
testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds)
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
testScheduler.runCurrent()
// THEN: The notification is not recognized as "seen" and is not filtered out.
assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
}
}
private fun runKeyguardCoordinatorTest(
testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
) {
val testScope = TestScope(UnconfinedTestDispatcher())
val keyguardCoordinator =
KeyguardCoordinator(
keyguardNotifVisibilityProvider,
keyguardRepository,
notifPipelineFlags,
testScope.backgroundScope,
sectionHeaderVisibilityProvider,
statusBarStateController,
)
keyguardCoordinator.attach(notifPipeline)
KeyguardCoordinatorTestScope(keyguardCoordinator, testScope).run {
testScheduler.advanceUntilIdle()
testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { testBlock() }
}
}
private inner class KeyguardCoordinatorTestScope(
private val keyguardCoordinator: KeyguardCoordinator,
private val scope: TestScope,
) : CoroutineScope by scope {
val testScheduler: TestCoroutineScheduler
get() = scope.testScheduler
val onStateChangeListener: Consumer<String> =
withArgCaptor {
verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
}
val unseenFilter: NotifFilter
get() = keyguardCoordinator.unseenNotifFilter
// TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and
// removed
val collectionListener: NotifCollectionListener by lazy {
withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) }
}
}
}