From 30a5744db044aca62b7f12ebe235464b9bd1bb02 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Tue, 21 Mar 2023 15:28:15 +0000 Subject: [PATCH] Hide Notes Quick Affordance when Direct Boot user is locked * Quick affordance should hide notes shortcut if direct boot user is locked; * Once the user unlocks, notes is accessible from lock screen again; For context: * Quick affordance only evaluates the `lockScreenState` if the shortcut is in the lock screen. If a user changes the shortcut in Settings, the shortcut is not on the lock screen and `lockScreenState` does not matter. * Notes is added as default shortcut for tablets but we keep it hidden. When a Stylus is used for the first time or the user set notes shortcut in Settings, `lockScreenState` will change to visible. * A user can change the lock screen shortcut in Settings by: Settings -> Wallpaper & Style & Lock Screen Shortcuts. Test: atest NoteTaskQuickAffordanceConfigTest Fixes: b/266746835 Change-Id: I48dccd7784c1e00168afa723a32bcb96bf9dd13a --- .../NoteTaskQuickAffordanceConfig.kt | 83 ++++++++---- .../NoteTaskQuickAffordanceConfigTest.kt | 123 ++++++++++++++---- 2 files changed, 155 insertions(+), 51 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt index 8aed9958a7a2a..2da5b76217237 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt @@ -18,6 +18,11 @@ package com.android.systemui.notetask.quickaffordance import android.content.Context import android.hardware.input.InputSettings +import android.os.Build +import android.os.UserManager +import android.util.Log +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -39,6 +44,7 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach class NoteTaskQuickAffordanceConfig @Inject @@ -46,6 +52,8 @@ constructor( context: Context, private val controller: NoteTaskController, private val stylusManager: StylusManager, + private val keyguardMonitor: KeyguardUpdateMonitor, + private val userManager: UserManager, private val lazyRepository: Lazy, @NoteTaskEnabledKey private val isEnabled: Boolean, ) : KeyguardQuickAffordanceConfig { @@ -61,17 +69,27 @@ constructor( // Due to a dependency cycle with KeyguardQuickAffordanceRepository, we need to lazily access // the repository when lockScreenState is accessed for the first time. override val lockScreenState by lazy { - val stylusEverUsedFlow = createStylusEverUsedFlow(context, stylusManager) - val configSelectedFlow = createConfigSelectedFlow(lazyRepository.get(), key) - combine(configSelectedFlow, stylusEverUsedFlow) { isSelected, isStylusEverUsed -> - if (isEnabled && (isSelected || isStylusEverUsed)) { - val contentDescription = ContentDescription.Resource(pickerNameResourceId) - val icon = Icon.Resource(pickerIconResourceId, contentDescription) - LockScreenState.Visible(icon) - } else { - LockScreenState.Hidden + val repository = lazyRepository.get() + val configSelectedFlow = repository.createConfigSelectedFlow(key) + val stylusEverUsedFlow = stylusManager.createStylusEverUsedFlow(context) + val userUnlockedFlow = userManager.createUserUnlockedFlow(keyguardMonitor) + combine(userUnlockedFlow, stylusEverUsedFlow, configSelectedFlow) { + isUserUnlocked, + isStylusEverUsed, + isConfigSelected -> + logDebug { "lockScreenState:isUserUnlocked=$isUserUnlocked" } + logDebug { "lockScreenState:isStylusEverUsed=$isStylusEverUsed" } + logDebug { "lockScreenState:isConfigSelected=$isConfigSelected" } + + if (isEnabled && isUserUnlocked && (isConfigSelected || isStylusEverUsed)) { + val contentDescription = ContentDescription.Resource(pickerNameResourceId) + val icon = Icon.Resource(pickerIconResourceId, contentDescription) + LockScreenState.Visible(icon) + } else { + LockScreenState.Hidden + } } - } + .onEach { state -> logDebug { "lockScreenState=$state" } } } override suspend fun getPickerScreenState() = @@ -82,27 +100,40 @@ constructor( } override fun onTriggered(expandable: Expandable?): OnTriggeredResult { - controller.showNoteTask( - entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE, - ) + controller.showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) return OnTriggeredResult.Handled } } -private fun createStylusEverUsedFlow(context: Context, stylusManager: StylusManager) = - callbackFlow { - trySendBlocking(InputSettings.isStylusEverUsed(context)) - val callback = - object : StylusManager.StylusCallback { - override fun onStylusFirstUsed() { - trySendBlocking(InputSettings.isStylusEverUsed(context)) - } +private fun UserManager.createUserUnlockedFlow(monitor: KeyguardUpdateMonitor) = callbackFlow { + trySendBlocking(isUserUnlocked) + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onUserUnlocked() { + trySendBlocking(isUserUnlocked) } - stylusManager.registerCallback(callback) - awaitClose { stylusManager.unregisterCallback(callback) } - } + } + monitor.registerCallback(callback) + awaitClose { monitor.removeCallback(callback) } +} -private fun createConfigSelectedFlow(repository: KeyguardQuickAffordanceRepository, key: String) = - repository.selections.map { selected -> +private fun StylusManager.createStylusEverUsedFlow(context: Context) = callbackFlow { + trySendBlocking(InputSettings.isStylusEverUsed(context)) + val callback = + object : StylusManager.StylusCallback { + override fun onStylusFirstUsed() { + trySendBlocking(InputSettings.isStylusEverUsed(context)) + } + } + registerCallback(callback) + awaitClose { unregisterCallback(callback) } +} + +private fun KeyguardQuickAffordanceRepository.createConfigSelectedFlow(key: String) = + selections.map { selected -> selected.values.flatten().any { selectedConfig -> selectedConfig.key == key } } + +private inline fun Any.logDebug(message: () -> String) { + if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName, message()) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt index d44012f31fa56..42ef2b5ad3ab3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.notetask.quickaffordance import android.hardware.input.InputSettings +import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import com.android.dx.mockito.inline.extended.ExtendedMockito @@ -33,6 +34,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepo import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint import com.android.systemui.stylus.StylusManager +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,6 +57,7 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { @Mock lateinit var controller: NoteTaskController @Mock lateinit var stylusManager: StylusManager @Mock lateinit var repository: KeyguardQuickAffordanceRepository + @Mock lateinit var userManager: UserManager private lateinit var mockitoSession: MockitoSession @@ -66,12 +69,6 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { .mockStatic(InputSettings::class.java) .strictness(Strictness.LENIENT) .startMocking() - - whenever(InputSettings.isStylusEverUsed(mContext)).then { true } - whenever(repository.selections).then { - val map = mapOf("" to listOf(createUnderTest())) - MutableStateFlow(map) - } } @After @@ -84,6 +81,8 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { context = context, controller = controller, stylusManager = stylusManager, + userManager = userManager, + keyguardMonitor = mock(), lazyRepository = { repository }, isEnabled = isEnabled, ) @@ -98,47 +97,101 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { ) ) + // region lockScreenState @Test - fun lockScreenState_stylusUsed_noCustomShortcutSelected_shouldEmitVisible() = runTest { - val underTest = createUnderTest() + fun lockScreenState_stylusUsed_userUnlocked_isSelected_shouldEmitVisible() = runTest { + TestConfig() + .setStylusEverUsed(true) + .setUserUnlocked(true) + .setConfigSelections(mock()) + val underTest = createUnderTest() val actual by collectLastValue(underTest.lockScreenState) assertThat(actual).isEqualTo(createLockScreenStateVisible()) } @Test - fun lockScreenState_noStylusEverUsed_noCustomShortcutSelected_shouldEmitVisible() = runTest { - whenever(InputSettings.isStylusEverUsed(mContext)).then { false } - val underTest = createUnderTest() + fun lockScreenState_stylusUnused_userUnlocked_isSelected_shouldEmitHidden() = runTest { + TestConfig() + .setStylusEverUsed(false) + .setUserUnlocked(true) + .setConfigSelections(mock()) + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test + fun lockScreenState_stylusUsed_userLocked_isSelected_shouldEmitHidden() = runTest { + TestConfig() + .setStylusEverUsed(true) + .setUserUnlocked(false) + .setConfigSelections(mock()) + + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test + fun lockScreenState_stylusUsed_userUnlocked_noSelected_shouldEmitVisible() = runTest { + TestConfig().setStylusEverUsed(true).setUserUnlocked(true).setConfigSelections() + + val underTest = createUnderTest() val actual by collectLastValue(underTest.lockScreenState) assertThat(actual).isEqualTo(createLockScreenStateVisible()) } @Test - fun lockScreenState_stylusUsed_customShortcutSelected_shouldEmitVisible() = runTest { - whenever(repository.selections).then { - val map = mapOf>() - MutableStateFlow(map) - } - val underTest = createUnderTest() + fun lockScreenState_stylusUnused_userUnlocked_noSelected_shouldEmitHidden() = runTest { + TestConfig().setStylusEverUsed(false).setUserUnlocked(true).setConfigSelections() + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test + fun lockScreenState_stylusUsed_userLocked_noSelected_shouldEmitHidden() = runTest { + TestConfig().setStylusEverUsed(true).setUserUnlocked(false).setConfigSelections() + + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test + fun lockScreenState_stylusUsed_userUnlocked_customSelections_shouldEmitVisible() = runTest { + TestConfig().setStylusEverUsed(true).setUserUnlocked(true).setConfigSelections(mock()) + + val underTest = createUnderTest() val actual by collectLastValue(underTest.lockScreenState) assertThat(actual).isEqualTo(createLockScreenStateVisible()) } @Test - fun lockScreenState_noIsStylusEverUsed_noCustomShortcutSelected_shouldEmitHidden() = runTest { - whenever(InputSettings.isStylusEverUsed(mContext)).then { false } - whenever(repository.selections).then { - val map = mapOf>() - MutableStateFlow(map) - } - val underTest = createUnderTest() + fun lockScreenState_stylusUnused_userUnlocked_customSelections_shouldEmitHidden() = runTest { + TestConfig().setStylusEverUsed(false).setUserUnlocked(true).setConfigSelections(mock()) + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test + fun lockScreenState_stylusUsed_userLocked_customSelections_shouldEmitHidden() = runTest { + TestConfig().setStylusEverUsed(true).setUserUnlocked(false).setConfigSelections(mock()) + + val underTest = createUnderTest() val actual by collectLastValue(underTest.lockScreenState) assertThat(actual).isEqualTo(LockScreenState.Hidden) @@ -146,12 +199,14 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { @Test fun lockScreenState_isNotEnabled_shouldEmitHidden() = runTest { - val underTest = createUnderTest(isEnabled = false) + TestConfig().setStylusEverUsed(true).setUserUnlocked(true).setConfigSelections() + val underTest = createUnderTest(isEnabled = false) val actual by collectLastValue(underTest.lockScreenState) assertThat(actual).isEqualTo(LockScreenState.Hidden) } + // endregion @Test fun onTriggered_shouldLaunchNoteTask() { @@ -161,4 +216,22 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) } + + private inner class TestConfig { + + fun setStylusEverUsed(value: Boolean) = also { + whenever(InputSettings.isStylusEverUsed(mContext)).thenReturn(value) + } + + fun setUserUnlocked(value: Boolean) = also { + whenever(userManager.isUserUnlocked).thenReturn(value) + } + + fun setConfigSelections(vararg values: KeyguardQuickAffordanceConfig) = also { + val slotKey = "bottom-right" + val configSnapshots = values.toList() + val map = mapOf(slotKey to configSnapshots) + whenever(repository.selections).thenReturn(MutableStateFlow(map)) + } + } }