diff options
10 files changed, 400 insertions, 37 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt index 44c41058f99d..bb2fbf73b2e5 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.theme.LocalAndroidColorScheme @@ -34,11 +35,13 @@ fun PlatformButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + colors: ButtonColors = filledButtonColors(), + verticalPadding: Dp = DefaultPlatformButtonVerticalPadding, content: @Composable RowScope.() -> Unit, ) { androidx.compose.material3.Button( - modifier = modifier.padding(vertical = 6.dp).height(36.dp), - colors = filledButtonColors(), + modifier = modifier.padding(vertical = verticalPadding).height(36.dp), + colors = colors, contentPadding = ButtonPaddings, onClick = onClick, enabled = enabled, @@ -52,13 +55,16 @@ fun PlatformOutlinedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + colors: ButtonColors = outlineButtonColors(), + border: BorderStroke? = outlineButtonBorder(), + verticalPadding: Dp = DefaultPlatformButtonVerticalPadding, content: @Composable RowScope.() -> Unit, ) { androidx.compose.material3.OutlinedButton( - modifier = modifier.padding(vertical = 6.dp).height(36.dp), + modifier = modifier.padding(vertical = verticalPadding).height(36.dp), enabled = enabled, - colors = outlineButtonColors(), - border = outlineButtonBorder(), + colors = colors, + border = border, contentPadding = ButtonPaddings, onClick = onClick, ) { @@ -71,6 +77,7 @@ fun PlatformTextButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + colors: ButtonColors = textButtonColors(), content: @Composable RowScope.() -> Unit, ) { androidx.compose.material3.TextButton( @@ -78,10 +85,11 @@ fun PlatformTextButton( modifier = modifier, enabled = enabled, content = content, - colors = textButtonColors(), + colors = colors, ) } +private val DefaultPlatformButtonVerticalPadding = 6.dp private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index a61e95931222..a9944f739975 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -24,18 +24,30 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -48,12 +60,18 @@ 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.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import com.android.compose.PlatformButton import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass @@ -62,6 +80,8 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.compose.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction @@ -70,6 +90,9 @@ import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.pow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -164,7 +187,7 @@ private fun Bouncer( Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(60.dp), - modifier = modifier.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp) + modifier = modifier.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 92.dp) ) { Crossfade( targetState = message, @@ -201,18 +224,20 @@ private fun Bouncer( } } - Button( - onClick = viewModel::onEmergencyServicesButtonClicked, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ), - ) { - Text( - text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), - style = MaterialTheme.typography.bodyMedium, - ) + if (viewModel.isEmergencyButtonVisible) { + Button( + onClick = viewModel::onEmergencyServicesButtonClicked, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + ) { + Text( + text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), + style = MaterialTheme.typography.bodyMedium, + ) + } } if (dialogMessage != null) { @@ -241,16 +266,133 @@ private fun Bouncer( /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */ @Composable private fun UserSwitcher( + viewModel: BouncerViewModel, modifier: Modifier = Modifier, ) { - Box(modifier) { - Text( - text = "TODO: the user switcher goes here", - modifier = Modifier.align(Alignment.Center) + val selectedUserImage by viewModel.selectedUserImage.collectAsState(null) + val dropdownItems by viewModel.userSwitcherDropdown.collectAsState(emptyList()) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier, + ) { + selectedUserImage?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(SelectedUserImageSize), + ) + } + + UserSwitcherDropdown( + items = dropdownItems, ) } } +@Composable +private fun UserSwitcherDropdown( + items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>, +) { + val (isDropdownExpanded, setDropdownExpanded) = remember { mutableStateOf(false) } + + items.firstOrNull()?.let { firstDropdownItem -> + Spacer(modifier = Modifier.height(40.dp)) + + Box { + PlatformButton( + modifier = + Modifier + // Remove the built-in padding applied inside PlatformButton: + .padding(vertical = 0.dp) + .width(UserSwitcherDropdownWidth) + .height(UserSwitcherDropdownHeight), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + onClick = { setDropdownExpanded(!isDropdownExpanded) }, + ) { + val context = LocalContext.current + Text( + text = checkNotNull(firstDropdownItem.text.loadText(context)), + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + } + + UserSwitcherDropdownMenu( + isExpanded = isDropdownExpanded, + items = items, + onDismissed = { setDropdownExpanded(false) }, + ) + } + } +} + +@Composable +private fun UserSwitcherDropdownMenu( + isExpanded: Boolean, + items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>, + onDismissed: () -> Unit, +) { + val context = LocalContext.current + + // TODO(b/303071855): once the FR is fixed, remove this composition local override. + MaterialTheme( + colorScheme = + MaterialTheme.colorScheme.copy( + surface = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(28.dp)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismissed, + offset = + DpOffset( + x = 0.dp, + y = -UserSwitcherDropdownHeight, + ), + modifier = Modifier.width(UserSwitcherDropdownWidth), + ) { + items.forEach { userSwitcherDropdownItem -> + DropdownMenuItem( + leadingIcon = { + Icon( + icon = userSwitcherDropdownItem.icon, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp), + ) + }, + text = { + Text( + text = checkNotNull(userSwitcherDropdownItem.text.loadText(context)), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + }, + onClick = { + onDismissed() + userSwitcherDropdownItem.onClick() + }, + ) + } + } + } +} + /** * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap * anywhere on the background to flip their positions. @@ -293,7 +435,7 @@ private fun SideBySide( 1f } else { // Since the user switcher is not first, the elements have to be swapped - // horizontally. In the case of RTL locales, this means pushing the user + // horizontally. In the case of RTL locale, this means pushing the user // switcher to the left, hence the negative number. -1f }, @@ -301,21 +443,28 @@ private fun SideBySide( ) UserSwitcher( + viewModel = viewModel, modifier = Modifier.fillMaxHeight().weight(1f).graphicsLayer { translationX = size.width * animatedOffset + alpha = animatedAlpha(animatedOffset) }, ) - Bouncer( - viewModel = viewModel, - dialogFactory = dialogFactory, + Box( modifier = Modifier.fillMaxHeight().weight(1f).graphicsLayer { // A negative sign is used to make sure this is offset in the direction that's // opposite of the direction that the user switcher is pushed in. translationX = -size.width * animatedOffset - }, - ) + alpha = animatedAlpha(animatedOffset) + } + ) { + Bouncer( + viewModel = viewModel, + dialogFactory = dialogFactory, + modifier = Modifier.widthIn(max = 400.dp).align(Alignment.BottomCenter), + ) + } } } @@ -330,6 +479,7 @@ private fun Stacked( modifier = modifier, ) { UserSwitcher( + viewModel = viewModel, modifier = Modifier.fillMaxWidth().weight(1f), ) Bouncer( @@ -343,3 +493,36 @@ private fun Stacked( interface BouncerSceneDialogFactory { operator fun invoke(): AlertDialog } + +/** + * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of + * the two reaches a stopping point but `0` in the middle of the transition. + */ +private fun animatedAlpha( + offset: Float, +): Float { + // Describes a curve that is made of two parabolic U-shaped curves mirrored horizontally around + // the y-axis. The U on the left runs between x = -1 and x = 0 while the U on the right runs + // between x = 0 and x = 1. + // + // The minimum values of the curves are at -0.5 and +0.5. + // + // Both U curves are vertically scaled such that they reach the points (-1, 1) and (1, 1). + // + // Breaking it down, it's y = a×(|x|-m)²+b, where: + // x: the offset + // y: the alpha + // m: x-axis center of the parabolic curves, where the minima are. + // b: y-axis offset to apply to the entire curve so the animation spends more time with alpha = + // 0. + // a: amplitude to scale the parabolic curves to reach y = 1 at x = -1, x = 0, and x = +1. + val m = 0.5f + val b = -0.25 + val a = (1 - b) / m.pow(2) + + return max(0f, (a * (abs(offset) - m).pow(2) + b).toFloat()) +} + +private val SelectedUserImageSize = 190.dp +private val UserSwitcherDropdownWidth = SelectedUserImageSize + 2 * 29.dp +private val UserSwitcherDropdownHeight = 60.dp diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt index 0cbfb68b6e93..7f3b794e2ac3 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt @@ -16,10 +16,16 @@ package com.android.systemui.bouncer.ui +import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule import dagger.Binds import dagger.Module -@Module +@Module( + includes = + [ + BouncerViewModelModule::class, + ], +) interface BouncerViewModule { /** Binds BouncerView to BouncerViewImpl and makes it injectable. */ @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index c98cf317bb73..2cb98d879e69 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -17,19 +17,29 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context +import android.graphics.Bitmap +import androidx.core.graphics.drawable.toBitmap import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.scene.shared.flag.SceneContainerFlags -import javax.inject.Inject +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.ui.viewmodel.UserActionViewModel +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel +import com.android.systemui.user.ui.viewmodel.UserViewModel +import dagger.Module +import dagger.Provides import kotlin.math.ceil import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -42,17 +52,53 @@ import kotlinx.coroutines.job import kotlinx.coroutines.launch /** Holds UI state and handles user input on bouncer UIs. */ -@SysUISingleton -class BouncerViewModel -@Inject -constructor( +class BouncerViewModel( @Application private val applicationContext: Context, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, private val bouncerInteractor: BouncerInteractor, authenticationInteractor: AuthenticationInteractor, flags: SceneContainerFlags, + private val telephonyInteractor: TelephonyInteractor, + selectedUser: Flow<UserViewModel>, + users: Flow<List<UserViewModel>>, + userSwitcherMenu: Flow<List<UserActionViewModel>>, ) { + val selectedUserImage: StateFlow<Bitmap?> = + selectedUser + .map { it.image.toBitmap() } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = + combine( + users, + userSwitcherMenu, + ) { users, actions -> + users.map { user -> + UserSwitcherDropdownItemViewModel( + icon = Icon.Loaded(user.image, contentDescription = null), + text = user.name, + onClick = user.onClicked ?: {}, + ) + } + + actions.map { action -> + UserSwitcherDropdownItemViewModel( + icon = Icon.Resource(action.iconResourceId, contentDescription = null), + text = Text.Resource(action.textResourceId), + onClick = action.onClicked, + ) + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList(), + ) + private val isInputEnabled: StateFlow<Boolean> = bouncerInteractor.isThrottled .map { !it } @@ -102,6 +148,9 @@ constructor( ), ) + val isEmergencyButtonVisible: Boolean + get() = telephonyInteractor.hasTelephonyRadio + init { if (flags.isEnabled()) { applicationScope.launch { @@ -200,4 +249,40 @@ constructor( */ val isUpdateAnimated: Boolean, ) + + data class UserSwitcherDropdownItemViewModel( + val icon: Icon, + val text: Text, + val onClick: () -> Unit, + ) +} + +@Module +object BouncerViewModelModule { + + @Provides + @SysUISingleton + fun viewModel( + @Application applicationContext: Context, + @Application applicationScope: CoroutineScope, + @Main mainDispatcher: CoroutineDispatcher, + bouncerInteractor: BouncerInteractor, + authenticationInteractor: AuthenticationInteractor, + flags: SceneContainerFlags, + telephonyInteractor: TelephonyInteractor, + userSwitcherViewModel: UserSwitcherViewModel, + ): BouncerViewModel { + return BouncerViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + mainDispatcher = mainDispatcher, + bouncerInteractor = bouncerInteractor, + authenticationInteractor = authenticationInteractor, + flags = flags, + telephonyInteractor = telephonyInteractor, + selectedUser = userSwitcherViewModel.selectedUser, + users = userSwitcherViewModel.users, + userSwitcherMenu = userSwitcherViewModel.menu, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt index 9c38dc0f8852..3b300249aac2 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -17,10 +17,13 @@ package com.android.systemui.telephony.data.repository +import android.content.Context +import android.content.pm.PackageManager import android.telephony.Annotation import android.telephony.TelephonyCallback import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.telephony.TelephonyListenerManager import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -30,6 +33,9 @@ import kotlinx.coroutines.flow.Flow interface TelephonyRepository { /** The state of the current call. */ @Annotation.CallState val callState: Flow<Int> + + /** Whether the device has a radio that can be used for telephony. */ + val hasTelephonyRadio: Boolean } /** @@ -43,6 +49,7 @@ interface TelephonyRepository { class TelephonyRepositoryImpl @Inject constructor( + @Application private val applicationContext: Context, private val manager: TelephonyListenerManager, ) : TelephonyRepository { @Annotation.CallState @@ -53,4 +60,7 @@ constructor( awaitClose { manager.removeCallStateListener(listener) } } + + override val hasTelephonyRadio: Boolean + get() = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt index 86ca33df24dd..4642f552615a 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -28,7 +28,11 @@ import kotlinx.coroutines.flow.Flow class TelephonyInteractor @Inject constructor( - repository: TelephonyRepository, + private val repository: TelephonyRepository, ) { @Annotation.CallState val callState: Flow<Int> = repository.callState + + /** Whether the device has a radio that can be used for telephony. */ + val hasTelephonyRadio: Boolean + get() = repository.hasTelephonyRadio } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 61952baba7b1..d3f83b1e18bd 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -17,10 +17,10 @@ package com.android.systemui.user.ui.viewmodel -import com.android.systemui.res.R import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.drawable.CircularDrawable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper @@ -42,6 +42,10 @@ constructor( private val guestUserInteractor: GuestUserInteractor, ) { + /** The currently selected user. */ + val selectedUser: Flow<UserViewModel> = + userInteractor.selectedUser.map { user -> toViewModel(user) } + /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt index 773a0d8ceb64..020903057eb3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -49,6 +49,7 @@ class TelephonyRepositoryImplTest : SysuiTestCase() { underTest = TelephonyRepositoryImpl( + applicationContext = context, manager = manager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 67777349ce12..499295c25883 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -17,6 +17,9 @@ package com.android.systemui.scene import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.util.Log import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.repository.AuthenticationRepository @@ -30,6 +33,7 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository @@ -51,12 +55,17 @@ import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.ui.viewmodel.UserActionViewModel +import com.android.systemui.user.ui.viewmodel.UserViewModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -209,7 +218,9 @@ class SceneTestUtils( fun bouncerViewModel( bouncerInteractor: BouncerInteractor, authenticationInteractor: AuthenticationInteractor, + users: List<UserViewModel> = createUsers(), ): BouncerViewModel { + Log.d("ALE", "users=$users") return BouncerViewModel( applicationContext = context, applicationScope = applicationScope(), @@ -217,6 +228,13 @@ class SceneTestUtils( bouncerInteractor = bouncerInteractor, authenticationInteractor = authenticationInteractor, flags = sceneContainerFlags, + selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }), + users = flowOf(users), + userSwitcherMenu = flowOf(createMenuActions()), + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), ) } @@ -232,6 +250,43 @@ class SceneTestUtils( return testScope.backgroundScope } + private fun createUsers( + count: Int = 3, + selectedIndex: Int = 0, + ): List<UserViewModel> { + check(selectedIndex in 0 until count) + + return buildList { + repeat(count) { index -> + add( + UserViewModel( + viewKey = index, + name = Text.Loaded("name_$index"), + image = BitmapDrawable(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)), + isSelectionMarkerVisible = index == selectedIndex, + alpha = 1f, + onClicked = {}, + ) + ) + } + } + } + + private fun createMenuActions(): List<UserActionViewModel> { + return buildList { + repeat(3) { index -> + add( + UserActionViewModel( + viewKey = index.toLong(), + iconResourceId = 0, + textResourceId = 0, + onClicked = {}, + ) + ) + } + } + } + companion object { fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel { return when (this) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt index 7c70846d4d76..992ac62fe3c4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt @@ -31,9 +31,16 @@ class FakeTelephonyRepository @Inject constructor() : TelephonyRepository { private val _callState = MutableStateFlow(0) override val callState: Flow<Int> = _callState.asStateFlow() + override var hasTelephonyRadio: Boolean = true + private set + fun setCallState(value: Int) { _callState.value = value } + + fun setHasRadio(hasRadio: Boolean) { + this.hasTelephonyRadio = hasRadio + } } @Module |