Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 30a5744d authored by Marcello Galhardo's avatar Marcello Galhardo
Browse files

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
parent a7763f5d
Loading
Loading
Loading
Loading
+57 −26
Original line number Diff line number Diff line
@@ -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,10 +69,19 @@ 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 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)
@@ -72,6 +89,7 @@ constructor(
                    LockScreenState.Hidden
                }
            }
            .onEach { state -> logDebug { "lockScreenState=$state" } }
    }

    override suspend fun getPickerScreenState() =
@@ -82,15 +100,24 @@ 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 {
private fun UserManager.createUserUnlockedFlow(monitor: KeyguardUpdateMonitor) = callbackFlow {
    trySendBlocking(isUserUnlocked)
    val callback =
        object : KeyguardUpdateMonitorCallback() {
            override fun onUserUnlocked() {
                trySendBlocking(isUserUnlocked)
            }
        }
    monitor.registerCallback(callback)
    awaitClose { monitor.removeCallback(callback) }
}

private fun StylusManager.createStylusEverUsedFlow(context: Context) = callbackFlow {
    trySendBlocking(InputSettings.isStylusEverUsed(context))
    val callback =
        object : StylusManager.StylusCallback {
@@ -98,11 +125,15 @@ private fun createStylusEverUsedFlow(context: Context, stylusManager: StylusMana
                trySendBlocking(InputSettings.isStylusEverUsed(context))
            }
        }
        stylusManager.registerCallback(callback)
        awaitClose { stylusManager.unregisterCallback(callback) }
    registerCallback(callback)
    awaitClose { unregisterCallback(callback) }
}

private fun createConfigSelectedFlow(repository: KeyguardQuickAffordanceRepository, key: String) =
    repository.selections.map { selected ->
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())
}
+95 −22
Original line number Diff line number Diff line
@@ -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 }
    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)
    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)
        }
    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))
        }
    }
}