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
This commit is contained in:
@@ -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<KeyguardQuickAffordanceRepository>,
|
||||
@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())
|
||||
}
|
||||
|
||||
@@ -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<NoteTaskQuickAffordanceConfig>())
|
||||
|
||||
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<NoteTaskQuickAffordanceConfig>())
|
||||
|
||||
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<NoteTaskQuickAffordanceConfig>())
|
||||
|
||||
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<String, List<KeyguardQuickAffordanceConfig>>()
|
||||
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<String, List<KeyguardQuickAffordanceConfig>>()
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user