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

Commit e240b52c authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes I789996d1,I762186ec,I880f8cde into main

* changes:
  Minor cleanup of dismiss action interactor to hide state that it doesn't have to expose.
  Add predictive back animation to the compose bouncer
  Add support for saving the preferred input side for the bouncer
parents 1a079278 4f82f66e
Loading
Loading
Loading
Loading
+41 −10
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,14 +62,15 @@ 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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
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
@@ -123,8 +126,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)
}
@@ -137,6 +140,7 @@ fun BouncerContent(
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier,
) {
    val scale by viewModel.scale.collectAsStateWithLifecycle()
    Box(
        // Allows the content within each of the layouts to react to the appearance and
        // disappearance of the IME, which is also known as the software keyboard.
@@ -144,7 +148,7 @@ fun BouncerContent(
        // Despite the keyboard only being part of the password bouncer, adding it at this level is
        // both necessary to properly handle the keyboard in all layouts and harmless in cases when
        // the keyboard isn't used (like the PIN or pattern auth methods).
        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent)
        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent).scale(scale)
    ) {
        when (layout) {
            BouncerSceneLayout.STANDARD_BOUNCER -> StandardLayout(viewModel = viewModel)
@@ -300,28 +304,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
@@ -726,7 +756,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