Loading packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +14 −3 Original line number Diff line number Diff line Loading @@ -142,7 +142,11 @@ private fun SceneScope.BouncerScene( modifier: Modifier = Modifier, ) { val backgroundColor = MaterialTheme.colorScheme.surface val layout = calculateLayout() val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState() val layout = calculateLayout( isSideBySideSupported = isSideBySideSupported, ) Box(modifier) { Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) { Loading Loading @@ -567,6 +571,11 @@ private fun SwappableLayout( /** * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap * anywhere on the background to flip their positions. * * In situations when [isUserSwitcherVisible] is `false`, one of two things may happen: either the * UI for the bouncer will be shown on its own, taking up one side, with the other side just being * empty space or, if that kind of "stand-alone side-by-side" isn't supported, the standard * rendering of the bouncer will be used instead of the side-by-side layout. */ @Composable private fun SideBySide( Loading Loading @@ -628,7 +637,9 @@ private fun Stacked( } @Composable private fun calculateLayout(): Layout { private fun calculateLayout( isSideBySideSupported: Boolean, ): Layout { val windowSizeClass = LocalWindowSizeClass.current val width = windowSizeClass.widthSizeClass val height = windowSizeClass.heightSizeClass Loading Loading @@ -657,7 +668,7 @@ private fun calculateLayout(): Layout { // Large and tall devices (i.e. tablet in portrait). isTall -> Layout.STACKED // Large and wide/square devices (i.e. tablet in landscape, unfolded). else -> Layout.SIDE_BY_SIDE else -> if (isSideBySideSupported) Layout.SIDE_BY_SIDE else Layout.STANDARD } } Loading packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +3 −2 Original line number Diff line number Diff line Loading @@ -29,14 +29,15 @@ import kotlinx.coroutines.flow.asStateFlow class BouncerRepository @Inject constructor( flags: FeatureFlagsClassic, private val flags: FeatureFlagsClassic, ) { private val _message = MutableStateFlow<String?>(null) /** The user-facing message to show in the bouncer. */ val message: StateFlow<String?> = _message.asStateFlow() /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) fun setMessage(message: String?) { _message.value = message Loading packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +2 −1 Original line number Diff line number Diff line Loading @@ -97,7 +97,8 @@ constructor( val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean = repository.isUserSwitcherVisible val isUserSwitcherVisible: Boolean get() = repository.isUserSwitcherVisible init { if (flags.isEnabled()) { Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +23 −1 Original line number Diff line number Diff line Loading @@ -100,7 +100,8 @@ class BouncerViewModel( initialValue = emptyList(), ) val isUserSwitcherVisible: Boolean = bouncerInteractor.isUserSwitcherVisible val isUserSwitcherVisible: Boolean get() = bouncerInteractor.isUserSwitcherVisible private val isInputEnabled: StateFlow<Boolean> = bouncerInteractor.isThrottled Loading Loading @@ -162,6 +163,23 @@ class BouncerViewModel( initialValue = null ) /** * Whether the "side-by-side" layout is supported. * * When presented on its own, without a user switcher (e.g. not on communal devices like * tablets, for example), some authentication method UIs don't do well if they're shown in the * side-by-side layout; these need to be shown with the standard layout so they can take up as * much width as possible. */ val isSideBySideSupported: StateFlow<Boolean> = authMethodViewModel .map { authMethod -> isSideBySideSupported(authMethod) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = isSideBySideSupported(authMethodViewModel.value), ) init { if (flags.isEnabled()) { applicationScope.launch { Loading Loading @@ -190,6 +208,10 @@ class BouncerViewModel( _throttlingDialogMessage.value = null } private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean { return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel } private fun toMessageViewModel( message: String?, isThrottled: Boolean, Loading packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +24 −0 Original line number Diff line number Diff line Loading @@ -20,9 +20,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.model.AuthenticationMethodModel import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage Loading Loading @@ -202,6 +204,28 @@ class BouncerViewModelTest : SysuiTestCase() { assertThat(throttlingDialogMessage).isNull() } @Test fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isSideBySideSupported).isTrue() utils.authenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) assertThat(isSideBySideSupported).isTrue() utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isSideBySideSupported).isTrue() utils.authenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) assertThat(isSideBySideSupported).isFalse() } private fun authMethodsToTest(): List<DomainLayerAuthenticationMethodModel> { return listOf( DomainLayerAuthenticationMethodModel.None, Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +14 −3 Original line number Diff line number Diff line Loading @@ -142,7 +142,11 @@ private fun SceneScope.BouncerScene( modifier: Modifier = Modifier, ) { val backgroundColor = MaterialTheme.colorScheme.surface val layout = calculateLayout() val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState() val layout = calculateLayout( isSideBySideSupported = isSideBySideSupported, ) Box(modifier) { Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) { Loading Loading @@ -567,6 +571,11 @@ private fun SwappableLayout( /** * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap * anywhere on the background to flip their positions. * * In situations when [isUserSwitcherVisible] is `false`, one of two things may happen: either the * UI for the bouncer will be shown on its own, taking up one side, with the other side just being * empty space or, if that kind of "stand-alone side-by-side" isn't supported, the standard * rendering of the bouncer will be used instead of the side-by-side layout. */ @Composable private fun SideBySide( Loading Loading @@ -628,7 +637,9 @@ private fun Stacked( } @Composable private fun calculateLayout(): Layout { private fun calculateLayout( isSideBySideSupported: Boolean, ): Layout { val windowSizeClass = LocalWindowSizeClass.current val width = windowSizeClass.widthSizeClass val height = windowSizeClass.heightSizeClass Loading Loading @@ -657,7 +668,7 @@ private fun calculateLayout(): Layout { // Large and tall devices (i.e. tablet in portrait). isTall -> Layout.STACKED // Large and wide/square devices (i.e. tablet in landscape, unfolded). else -> Layout.SIDE_BY_SIDE else -> if (isSideBySideSupported) Layout.SIDE_BY_SIDE else Layout.STANDARD } } Loading
packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +3 −2 Original line number Diff line number Diff line Loading @@ -29,14 +29,15 @@ import kotlinx.coroutines.flow.asStateFlow class BouncerRepository @Inject constructor( flags: FeatureFlagsClassic, private val flags: FeatureFlagsClassic, ) { private val _message = MutableStateFlow<String?>(null) /** The user-facing message to show in the bouncer. */ val message: StateFlow<String?> = _message.asStateFlow() /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) fun setMessage(message: String?) { _message.value = message Loading
packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +2 −1 Original line number Diff line number Diff line Loading @@ -97,7 +97,8 @@ constructor( val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean = repository.isUserSwitcherVisible val isUserSwitcherVisible: Boolean get() = repository.isUserSwitcherVisible init { if (flags.isEnabled()) { Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +23 −1 Original line number Diff line number Diff line Loading @@ -100,7 +100,8 @@ class BouncerViewModel( initialValue = emptyList(), ) val isUserSwitcherVisible: Boolean = bouncerInteractor.isUserSwitcherVisible val isUserSwitcherVisible: Boolean get() = bouncerInteractor.isUserSwitcherVisible private val isInputEnabled: StateFlow<Boolean> = bouncerInteractor.isThrottled Loading Loading @@ -162,6 +163,23 @@ class BouncerViewModel( initialValue = null ) /** * Whether the "side-by-side" layout is supported. * * When presented on its own, without a user switcher (e.g. not on communal devices like * tablets, for example), some authentication method UIs don't do well if they're shown in the * side-by-side layout; these need to be shown with the standard layout so they can take up as * much width as possible. */ val isSideBySideSupported: StateFlow<Boolean> = authMethodViewModel .map { authMethod -> isSideBySideSupported(authMethod) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = isSideBySideSupported(authMethodViewModel.value), ) init { if (flags.isEnabled()) { applicationScope.launch { Loading Loading @@ -190,6 +208,10 @@ class BouncerViewModel( _throttlingDialogMessage.value = null } private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean { return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel } private fun toMessageViewModel( message: String?, isThrottled: Boolean, Loading
packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +24 −0 Original line number Diff line number Diff line Loading @@ -20,9 +20,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.model.AuthenticationMethodModel import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage Loading Loading @@ -202,6 +204,28 @@ class BouncerViewModelTest : SysuiTestCase() { assertThat(throttlingDialogMessage).isNull() } @Test fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isSideBySideSupported).isTrue() utils.authenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) assertThat(isSideBySideSupported).isTrue() utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isSideBySideSupported).isTrue() utils.authenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) assertThat(isSideBySideSupported).isFalse() } private fun authMethodsToTest(): List<DomainLayerAuthenticationMethodModel> { return listOf( DomainLayerAuthenticationMethodModel.None, Loading