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

Commit c64ab0a3 authored by Chandru S's avatar Chandru S
Browse files

Add support for saving the preferred input side for the bouncer

 - Preferred bouncer input side is saved as a setting, if this value is not set, then the following applies:
   1. For devices with config_enableBouncerUserSwitcher (tablets), we default to the right side.
   2. For devices with can_use_one_handed_bouncer set to true in config and config_enableBouncerUserSwitcher set to false, we default to left side input (foldables)
 - When SIM auth method is active, then bouncer switcher will not be visible
 - Side by side mode is supported whenever user switcher is visible, or when can_use_one_handed_bouncer is set in the config and auth method is not password.
 - Adds falsing support for double taps to switch input sides on the bouncer.

Fixes: 375206096
Fixes: 375206804
Test: verified manually,
 1. Use a foldable in unfolded mode
 2. set auth method to pin/pattern
 3. go to bouncer, input should be on the left side
 4. double tap on the empty side of the bouncer, input should move to that side
 5. keep doing this for a few times, falsing should not reject double taps and move back to the lockscreen
 6. leave the input on the right side of the bouncer
 7. unlock the device and come back, it should still be on the right side
   verified manually on a tablet
 1. Go to bouncer with auth method as pin/pattern/password
 2. user switcher should be shown on the left side and bouncer input on the right side
 3. repeat the above steps and verify input side setting is saved.
Flag: com.android.systemui.compose_bouncer
Change-Id: I880f8cde571a2c9dfef27f7f8e80d112a8898303
parent 000bdcbd
Loading
Loading
Loading
Loading
+38 −9
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -60,7 +62,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -68,6 +69,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -122,8 +124,8 @@ fun BouncerContent(
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier = Modifier,
) {
    val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsStateWithLifecycle()
    val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
    val isOneHandedModeSupported by viewModel.isOneHandedModeSupported.collectAsStateWithLifecycle()
    val layout = calculateLayout(isOneHandedModeSupported = isOneHandedModeSupported)

    BouncerContent(layout, viewModel, dialogFactory, modifier)
}
@@ -299,28 +301,54 @@ private fun BesideUserSwitcherLayout(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
    val layoutDirection = LocalLayoutDirection.current
    val isLeftToRight = layoutDirection == LayoutDirection.Ltr
    val (isSwapped, setSwapped) = rememberSaveable(isLeftToRight) { mutableStateOf(!isLeftToRight) }
    val isLeftToRight = LocalLayoutDirection.current == LayoutDirection.Ltr
    val isInputPreferredOnLeftSide by
        viewModel.isInputPreferredOnLeftSide.collectAsStateWithLifecycle()
    // Swaps the order of user switcher and bouncer input area
    // Default layout is assumed as user switcher followed by bouncer input area in the direction
    // of layout.
    val isSwapped = isLeftToRight == isInputPreferredOnLeftSide
    val isHeightExpanded =
        LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
    val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()

    var swapAnimationEnd by remember { mutableStateOf(false) }

    fun wasEventOnNonInputHalfOfScreen(x: Float, totalWidth: Int): Boolean {
        // Default layout is assumed as user switcher followed by bouncer input area in
        // the direction of layout. Swapped layout means that bouncer input area is first, followed
        // by user switcher in the direction of layout.
        val halfWidth = totalWidth / 2
        return if (x > halfWidth) {
            isLeftToRight && isSwapped
        } else {
            isLeftToRight && !isSwapped
        }
    }

    Row(
        modifier =
            modifier
                .pointerInput(Unit) {
                .pointerInput(isSwapped, isInputPreferredOnLeftSide) {
                    detectTapGestures(
                        onDoubleTap = { offset ->
                            // Depending on where the user double tapped, switch the elements such
                            // that the non-swapped element is closer to the side that was double
                            // tapped.
                            setSwapped(offset.x < size.width / 2)
                            viewModel.onDoubleTap(
                                wasEventOnNonInputHalfOfScreen(offset.x, size.width)
                            )
                        }
                    )
                }
                .pointerInput(isSwapped, isInputPreferredOnLeftSide) {
                    awaitEachGesture {
                        val downEvent: PointerInputChange = awaitFirstDown()
                        viewModel.onDown(
                            wasEventOnNonInputHalfOfScreen(downEvent.position.x, size.width)
                        )
                    }
                }
                .testTag("BesideUserSwitcherLayout")
                .motionTestValues {
                    swapAnimationEnd exportAs BouncerMotionTestKeys.swapAnimationEnd
@@ -723,7 +751,8 @@ private fun Dialog(
/** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
@Composable
private fun UserSwitcher(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    if (!viewModel.isUserSwitcherVisible) {
    val isUserSwitcherVisible by viewModel.isUserSwitcherVisible.collectAsStateWithLifecycle()
    if (!isUserSwitcherVisible) {
        // Take up the same space as the user switcher normally would, but with nothing inside it.
        Box(modifier = modifier)
        return
+4 −6
Original line number Diff line number Diff line
@@ -26,19 +26,17 @@ import com.android.systemui.bouncer.ui.helper.calculateLayoutInternal

/**
 * Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If
 * [isSideBySideSupported] is `false`, then [BouncerSceneLayout.BESIDE_USER_SWITCHER] is replaced by
 * [BouncerSceneLayout.STANDARD_BOUNCER].
 * [isOneHandedModeSupported] is `false`, then [BouncerSceneLayout.BESIDE_USER_SWITCHER] is replaced
 * by [BouncerSceneLayout.STANDARD_BOUNCER].
 */
@Composable
fun calculateLayout(
    isSideBySideSupported: Boolean,
): BouncerSceneLayout {
fun calculateLayout(isOneHandedModeSupported: Boolean): BouncerSceneLayout {
    val windowSizeClass = LocalWindowSizeClass.current

    return calculateLayoutInternal(
        width = windowSizeClass.widthSizeClass.toEnum(),
        height = windowSizeClass.heightSizeClass.toEnum(),
        isSideBySideSupported = isSideBySideSupported,
        isOneHandedModeSupported = isOneHandedModeSupported,
    )
}

+84 −6
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.bouncer.domain.interactor

import android.content.testableContext
import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.uiEventLoggerFake
@@ -26,16 +28,22 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.BouncerInputSide
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.fakeGlobalSettings
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -58,7 +66,8 @@ class BouncerInteractorTest : SysuiTestCase() {
    private val authenticationInteractor = kosmos.authenticationInteractor
    private val uiEventLoggerFake = kosmos.uiEventLoggerFake

    private lateinit var underTest: BouncerInteractor
    private val underTest: BouncerInteractor by lazy { kosmos.bouncerInteractor }
    private val testableResources by lazy { kosmos.testableContext.orCreateTestableResources }

    @Before
    fun setUp() {
@@ -70,8 +79,6 @@ class BouncerInteractorTest : SysuiTestCase() {
        overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN)
        overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
        overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)

        underTest = kosmos.bouncerInteractor
    }

    @Test
@@ -116,7 +123,7 @@ class BouncerInteractorTest : SysuiTestCase() {
            assertThat(
                    underTest.authenticate(
                        FakeAuthenticationRepository.DEFAULT_PIN,
                        tryAutoConfirm = true
                        tryAutoConfirm = true,
                    )
                )
                .isEqualTo(AuthenticationResult.SUCCEEDED)
@@ -141,7 +148,7 @@ class BouncerInteractorTest : SysuiTestCase() {
            assertThat(
                    underTest.authenticate(
                        FakeAuthenticationRepository.DEFAULT_PIN,
                        tryAutoConfirm = true
                        tryAutoConfirm = true,
                    )
                )
                .isEqualTo(AuthenticationResult.SKIPPED)
@@ -209,7 +216,7 @@ class BouncerInteractorTest : SysuiTestCase() {
            val tooShortPattern =
                FakeAuthenticationRepository.PATTERN.subList(
                    0,
                    kosmos.fakeAuthenticationRepository.minPatternLength - 1
                    kosmos.fakeAuthenticationRepository.minPatternLength - 1,
                )
            assertThat(underTest.authenticate(tooShortPattern))
                .isEqualTo(AuthenticationResult.SKIPPED)
@@ -292,6 +299,77 @@ class BouncerInteractorTest : SysuiTestCase() {
            assertThat(isFaceAuthRunning).isFalse()
        }

    @Test
    fun verifyOneHandedModeUsesTheConfigValue() =
        testScope.runTest {
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Pin
            )
            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false)
            val oneHandedModelSupported by collectLastValue(underTest.isOneHandedModeSupported)

            assertThat(oneHandedModelSupported).isFalse()

            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true)
            kosmos.fakeConfigurationRepository.onAnyConfigurationChange()
            runCurrent()

            assertThat(oneHandedModelSupported).isTrue()

            testableResources.removeOverride(R.bool.can_use_one_handed_bouncer)
        }

    @Test
    fun verifyPreferredInputSideUsesTheSettingValue_Left() =
        testScope.runTest {
            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
            kosmos.bouncerRepository.setPreferredBouncerInputSide(BouncerInputSide.LEFT)
            runCurrent()

            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
        }

    @Test
    fun verifyPreferredInputSideUsesTheSettingValue_Right() =
        testScope.runTest {
            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
            underTest.setPreferredBouncerInputSide(BouncerInputSide.RIGHT)
            runCurrent()

            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.RIGHT)

            underTest.setPreferredBouncerInputSide(BouncerInputSide.LEFT)
            runCurrent()

            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
        }

    @Test
    fun preferredInputSide_defaultsToRight_whenUserSwitcherIsEnabled() =
        testScope.runTest {
            testableResources.addOverride(R.bool.config_enableBouncerUserSwitcher, true)
            kosmos.fakeFeatureFlagsClassic.set(FULL_SCREEN_USER_SWITCHER, true)
            kosmos.bouncerRepository.preferredBouncerInputSide.value = null
            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)

            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.RIGHT)
            testableResources.removeOverride(R.bool.config_enableBouncerUserSwitcher)
        }

    @Test
    fun preferredInputSide_defaultsToLeft_whenUserSwitcherIsNotEnabledAndOneHandedModeIsEnabled() =
        testScope.runTest {
            testableResources.addOverride(R.bool.config_enableBouncerUserSwitcher, false)
            kosmos.fakeFeatureFlagsClassic.set(FULL_SCREEN_USER_SWITCHER, true)
            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true)
            kosmos.fakeGlobalSettings.putInt(ONE_HANDED_KEYGUARD_SIDE, -1)
            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)

            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
            testableResources.removeOverride(R.bool.config_enableBouncerUserSwitcher)
            testableResources.removeOverride(R.bool.can_use_one_handed_bouncer)
        }

    companion object {
        private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
        private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
+15 −8
Original line number Diff line number Diff line
@@ -25,10 +25,10 @@ import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD_BOUNCE
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import org.junit.Test
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import org.junit.runner.RunWith
import platform.test.runner.parameterized.Parameter
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
import org.junit.runner.RunWith

@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@@ -41,6 +41,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
            height = SizeClass.EXPANDED,
            naturallyHeld = Vertically,
        )

    data object Tablet :
        Device(
            name = "tablet",
@@ -48,6 +49,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
            height = SizeClass.MEDIUM,
            naturallyHeld = Horizontally,
        )

    data object Folded :
        Device(
            name = "folded",
@@ -55,6 +57,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
            height = SizeClass.MEDIUM,
            naturallyHeld = Vertically,
        )

    data object Unfolded :
        Device(
            name = "unfolded",
@@ -64,6 +67,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
            widthWhenUnnaturallyHeld = SizeClass.MEDIUM,
            heightWhenUnnaturallyHeld = SizeClass.MEDIUM,
        )

    data object TallerFolded :
        Device(
            name = "taller folded",
@@ -71,6 +75,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
            height = SizeClass.EXPANDED,
            naturallyHeld = Vertically,
        )

    data object TallerUnfolded :
        Device(
            name = "taller unfolded",
@@ -131,7 +136,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
                                TestCase(
                                    device = device,
                                    held = device.naturallyHeld,
                                    isSideBySideSupported = false,
                                    isOneHandedModeSupported = false,
                                    expected = STANDARD_BOUNCER,
                                )
                            )
@@ -151,7 +156,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
                                TestCase(
                                    device = device,
                                    held = device.naturallyHeld.flip(),
                                    isSideBySideSupported = false,
                                    isOneHandedModeSupported = false,
                                    expected = STANDARD_BOUNCER,
                                )
                            )
@@ -170,7 +175,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
                        calculateLayoutInternal(
                            width = device.width(whenHeld = held),
                            height = device.height(whenHeld = held),
                            isSideBySideSupported = isSideBySideSupported,
                            isOneHandedModeSupported = isOneHandedModeSupported,
                        )
                    )
                    .isEqualTo(expected)
@@ -182,7 +187,7 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
        val device: Device,
        val held: Held,
        val expected: BouncerSceneLayout,
        val isSideBySideSupported: Boolean = true,
        val isOneHandedModeSupported: Boolean = true,
    ) {
        override fun toString(): String {
            return buildString {
@@ -190,8 +195,8 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
                append(" width: ${device.width(held).name.lowercase(Locale.US)}")
                append(" height: ${device.height(held).name.lowercase(Locale.US)}")
                append(" when held $held")
                if (!isSideBySideSupported) {
                    append(" (side-by-side not supported)")
                if (!isOneHandedModeSupported) {
                    append(" (one-handed-mode not supported)")
                }
            }
        }
@@ -242,11 +247,13 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
    sealed class Held {
        abstract fun flip(): Held
    }

    data object Vertically : Held() {
        override fun flip(): Held {
            return Horizontally
        }
    }

    data object Horizontally : Held() {
        override fun flip(): Held {
            return Vertically
+22 −6
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.bouncer.ui.viewmodel

import android.content.testableContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -35,6 +36,7 @@ import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -166,21 +168,35 @@ class BouncerSceneContentViewModelTest : SysuiTestCase() {
        }

    @Test
    fun isSideBySideSupported() =
    fun isOneHandedModeSupported() =
        testScope.runTest {
            val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
            val isOneHandedModeSupported by collectLastValue(underTest.isOneHandedModeSupported)
            kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
            kosmos.testableContext.orCreateTestableResources.addOverride(
                R.bool.config_enableBouncerUserSwitcher,
                true,
            )
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
            assertThat(isSideBySideSupported).isTrue()
            assertThat(isOneHandedModeSupported).isTrue()
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password)
            assertThat(isSideBySideSupported).isTrue()
            assertThat(isOneHandedModeSupported).isTrue()

            kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
            kosmos.testableContext.orCreateTestableResources.addOverride(
                R.bool.can_use_one_handed_bouncer,
                true,
            )
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
            assertThat(isSideBySideSupported).isTrue()
            assertThat(isOneHandedModeSupported).isTrue()

            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password)
            assertThat(isSideBySideSupported).isFalse()
            assertThat(isOneHandedModeSupported).isFalse()
            kosmos.testableContext.orCreateTestableResources.removeOverride(
                R.bool.config_enableBouncerUserSwitcher
            )
            kosmos.testableContext.orCreateTestableResources.removeOverride(
                R.bool.can_use_one_handed_bouncer
            )
        }

    @Test
Loading