[flexiglass] Adds user switcher to the landscape bouncer scene.
Bug: 299343639
Test: scene screenshots and video on the attached bug.
Change-Id: If138bfb045103139bf1b4eb1783eba51ee519150
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 44c4105..bb2fbf7 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.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 @@
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 @@
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 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
+ colors: ButtonColors = textButtonColors(),
content: @Composable RowScope.() -> Unit,
) {
androidx.compose.material3.TextButton(
@@ -78,10 +85,11 @@
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 a61e959..a9944f7 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.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.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.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.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 @@
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 @@
}
}
- 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 @@
/** 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 @@
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 @@
)
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 @@
modifier = modifier,
) {
UserSwitcher(
+ viewModel = viewModel,
modifier = Modifier.fillMaxWidth().weight(1f),
)
Bouncer(
@@ -343,3 +493,36 @@
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 0cbfb68..7f3b794 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 c98cf31..2cb98d8 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.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 @@
),
)
+ val isEmergencyButtonVisible: Boolean
+ get() = telephonyInteractor.hasTelephonyRadio
+
init {
if (flags.isEnabled()) {
applicationScope.launch {
@@ -200,4 +249,40 @@
*/
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 9c38dc0f..3b300249 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 @@
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 @@
class TelephonyRepositoryImpl
@Inject
constructor(
+ @Application private val applicationContext: Context,
private val manager: TelephonyListenerManager,
) : TelephonyRepository {
@Annotation.CallState
@@ -53,4 +60,7 @@
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 86ca33d..4642f55 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 @@
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 61952ba..d3f83b1 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 @@
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 773a0d8..0209030 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 @@
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 179206f..d8a6b1a 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.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
@@ -53,12 +57,17 @@
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
@@ -219,7 +228,9 @@
fun bouncerViewModel(
bouncerInteractor: BouncerInteractor,
authenticationInteractor: AuthenticationInteractor,
+ users: List<UserViewModel> = createUsers(),
): BouncerViewModel {
+ Log.d("ALE", "users=$users")
return BouncerViewModel(
applicationContext = context,
applicationScope = applicationScope(),
@@ -227,6 +238,13 @@
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
flags = sceneContainerFlags,
+ selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }),
+ users = flowOf(users),
+ userSwitcherMenu = flowOf(createMenuActions()),
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = FakeTelephonyRepository(),
+ ),
)
}
@@ -242,6 +260,43 @@
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 7c70846..992ac62 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 @@
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