summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt220
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt77
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserActionViewModel.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt199
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserViewModel.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt297
6 files changed, 862 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
new file mode 100644
index 000000000000..83a3d0d0457a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.binder
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.helper.widget.Flow as FlowWidget
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.R
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.user.UserSwitcherPopupMenu
+import com.android.systemui.user.UserSwitcherRootView
+import com.android.systemui.user.ui.viewmodel.UserActionViewModel
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.util.children
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+
+/** Binds a user switcher to its view-model. */
+object UserSwitcherViewBinder {
+
+ private const val USER_VIEW_TAG = "user_view"
+
+ /** Binds the given view to the given view-model. */
+ fun bind(
+ view: ViewGroup,
+ viewModel: UserSwitcherViewModel,
+ lifecycleOwner: LifecycleOwner,
+ layoutInflater: LayoutInflater,
+ falsingCollector: FalsingCollector,
+ onFinish: () -> Unit,
+ ) {
+ val rootView: UserSwitcherRootView = view.requireViewById(R.id.user_switcher_root)
+ val flowWidget: FlowWidget = view.requireViewById(R.id.flow)
+ val addButton: View = view.requireViewById(R.id.add)
+ val cancelButton: View = view.requireViewById(R.id.cancel)
+ val popupMenuAdapter = MenuAdapter(layoutInflater)
+ var popupMenu: UserSwitcherPopupMenu? = null
+
+ rootView.touchHandler =
+ object : Gefingerpoken {
+ override fun onTouchEvent(ev: MotionEvent?): Boolean {
+ falsingCollector.onTouchEvent(ev)
+ return false
+ }
+ }
+ addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() }
+ cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() }
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ viewModel.isFinishRequested
+ .filter { it }
+ .collect {
+ onFinish()
+ viewModel.onFinished()
+ }
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } }
+
+ launch {
+ viewModel.isMenuVisible.collect { isVisible ->
+ if (isVisible && popupMenu?.isShowing != true) {
+ popupMenu?.dismiss()
+ // Use post to make sure we show the popup menu *after* the activity is
+ // ready to show one to avoid a WindowManager$BadTokenException.
+ view.post {
+ popupMenu =
+ createAndShowPopupMenu(
+ context = view.context,
+ anchorView = addButton,
+ adapter = popupMenuAdapter,
+ onDismissed = viewModel::onMenuClosed,
+ )
+ }
+ } else if (!isVisible && popupMenu?.isShowing == true) {
+ popupMenu?.dismiss()
+ popupMenu = null
+ }
+ }
+ }
+
+ launch {
+ viewModel.menu.collect { menuViewModels ->
+ popupMenuAdapter.setItems(menuViewModels)
+ }
+ }
+
+ launch {
+ viewModel.maximumUserColumns.collect { maximumColumns ->
+ flowWidget.setMaxElementsWrap(maximumColumns)
+ }
+ }
+
+ launch {
+ viewModel.users.collect { users ->
+ val viewPool =
+ view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList()
+ viewPool.forEach { view.removeView(it) }
+ users.forEach { userViewModel ->
+ val userView =
+ if (viewPool.isNotEmpty()) {
+ viewPool.removeAt(0)
+ } else {
+ val inflatedView =
+ layoutInflater.inflate(
+ R.layout.user_switcher_fullscreen_item,
+ view,
+ false,
+ )
+ inflatedView.tag = USER_VIEW_TAG
+ inflatedView
+ }
+ userView.id = View.generateViewId()
+ view.addView(userView)
+ flowWidget.addView(userView)
+ UserViewBinder.bind(
+ view = userView,
+ viewModel = userViewModel,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun createAndShowPopupMenu(
+ context: Context,
+ anchorView: View,
+ adapter: MenuAdapter,
+ onDismissed: () -> Unit,
+ ): UserSwitcherPopupMenu {
+ return UserSwitcherPopupMenu(context).apply {
+ this.anchorView = anchorView
+ setAdapter(adapter)
+ setOnDismissListener { onDismissed() }
+ setOnItemClickListener { _, _, position, _ ->
+ val itemPositionExcludingHeader = position - 1
+ adapter.getItem(itemPositionExcludingHeader).onClicked()
+ }
+
+ show()
+ }
+ }
+
+ /** Adapter for the menu that can be opened. */
+ private class MenuAdapter(
+ private val layoutInflater: LayoutInflater,
+ ) : BaseAdapter() {
+
+ private val items = mutableListOf<UserActionViewModel>()
+
+ override fun getCount(): Int {
+ return items.size
+ }
+
+ override fun getItem(position: Int): UserActionViewModel {
+ return items[position]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return getItem(position).viewKey
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view =
+ convertView
+ ?: layoutInflater.inflate(
+ R.layout.user_switcher_fullscreen_popup_item,
+ parent,
+ false
+ )
+ val viewModel = getItem(position)
+ view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId)
+ view.requireViewById<TextView>(R.id.text).text =
+ view.resources.getString(viewModel.textResourceId)
+ return view
+ }
+
+ fun setItems(items: List<UserActionViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt
new file mode 100644
index 000000000000..e78807e675b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.binder
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.LayerDrawable
+import android.view.View
+import android.widget.ImageView
+import androidx.core.content.res.ResourcesCompat
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.common.ui.binder.TextViewBinder
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+
+/** Binds a user view to its view-model. */
+object UserViewBinder {
+ /** Binds the given view to the given view-model. */
+ fun bind(view: View, viewModel: UserViewModel) {
+ TextViewBinder.bind(view.requireViewById(R.id.user_switcher_text), viewModel.name)
+ view
+ .requireViewById<ImageView>(R.id.user_switcher_icon)
+ .setImageDrawable(getSelectableDrawable(view.context, viewModel))
+ view.alpha = viewModel.alpha
+ if (viewModel.onClicked != null) {
+ view.setOnClickListener { viewModel.onClicked.invoke() }
+ } else {
+ view.setOnClickListener(null)
+ }
+ }
+
+ private fun getSelectableDrawable(context: Context, viewModel: UserViewModel): Drawable {
+ val layerDrawable =
+ checkNotNull(
+ ResourcesCompat.getDrawable(
+ context.resources,
+ R.drawable.user_switcher_icon_large,
+ context.theme,
+ )
+ )
+ .mutate() as LayerDrawable
+ if (viewModel.isSelectionMarkerVisible) {
+ (layerDrawable.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
+ val stroke =
+ context.resources.getDimensionPixelSize(
+ R.dimen.user_switcher_icon_selected_width
+ )
+ val color =
+ Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.colorAccentPrimary
+ )
+
+ setStroke(stroke, color)
+ }
+ }
+
+ layerDrawable.setDrawableByLayerId(R.id.user_avatar, viewModel.image)
+ return layerDrawable
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserActionViewModel.kt
new file mode 100644
index 000000000000..149b1ffdaff0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserActionViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.viewmodel
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/** Models UI state for an action that can be performed on a user. */
+data class UserActionViewModel(
+ /**
+ * Key to use with the view or compose system to keep track of the view/composable across
+ * changes to the collection of [UserActionViewModel] instances.
+ */
+ val viewKey: Long,
+ @DrawableRes val iconResourceId: Int,
+ @StringRes val textResourceId: Int,
+ val onClicked: () -> Unit,
+)
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
new file mode 100644
index 000000000000..66ce01b7a86e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.systemui.R
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+/** Models UI state for the user switcher feature. */
+class UserSwitcherViewModel
+private constructor(
+ private val userInteractor: UserInteractor,
+ private val powerInteractor: PowerInteractor,
+) : ViewModel() {
+
+ /** On-device users. */
+ val users: Flow<List<UserViewModel>> =
+ userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
+
+ /** The maximum number of columns that the user selection grid should use. */
+ val maximumUserColumns: Flow<Int> =
+ users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
+
+ /** Whether the button to open the user action menu is visible. */
+ val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
+
+ private val _isMenuVisible = MutableStateFlow(false)
+ /**
+ * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
+ * consumer must invoke [onMenuClosed].
+ */
+ val isMenuVisible: Flow<Boolean> = _isMenuVisible
+ /** The user action menu. */
+ val menu: Flow<List<UserActionViewModel>> =
+ userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
+
+ private val hasCancelButtonBeenClicked = MutableStateFlow(false)
+
+ /**
+ * Whether the observer should finish the experience. Once consumed, [onFinished] must be called
+ * by the consumer.
+ */
+ val isFinishRequested: Flow<Boolean> = createFinishRequestedFlow()
+
+ /** Notifies that the user has clicked the cancel button. */
+ fun onCancelButtonClicked() {
+ hasCancelButtonBeenClicked.value = true
+ }
+
+ /**
+ * Notifies that the user experience is finished.
+ *
+ * Call this after consuming [isFinishRequested] with a `true` value in order to mark it as
+ * consumed such that the next consumer doesn't immediately finish itself.
+ */
+ fun onFinished() {
+ hasCancelButtonBeenClicked.value = false
+ }
+
+ /** Notifies that the user has clicked the "open menu" button. */
+ fun onOpenMenuButtonClicked() {
+ _isMenuVisible.value = true
+ }
+
+ /**
+ * Notifies that the user has dismissed or closed the user action menu.
+ *
+ * Call this after consuming [isMenuVisible] with a `true` value in order to reset it to `false`
+ * such that the next consumer doesn't immediately show the menu again.
+ */
+ fun onMenuClosed() {
+ _isMenuVisible.value = false
+ }
+
+ private fun createFinishRequestedFlow(): Flow<Boolean> {
+ var mostRecentSelectedUserId: Int? = null
+ var mostRecentIsInteractive: Boolean? = null
+
+ return combine(
+ // When the user is switched, we should finish.
+ userInteractor.selectedUser
+ .map { it.id }
+ .map {
+ val selectedUserChanged =
+ mostRecentSelectedUserId != null && mostRecentSelectedUserId != it
+ mostRecentSelectedUserId = it
+ selectedUserChanged
+ },
+ // When the screen turns off, we should finish.
+ powerInteractor.isInteractive.map {
+ val screenTurnedOff = mostRecentIsInteractive == true && !it
+ mostRecentIsInteractive = it
+ screenTurnedOff
+ },
+ // When the cancel button is clicked, we should finish.
+ hasCancelButtonBeenClicked,
+ ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
+ selectedUserChanged || screenTurnedOff || cancelButtonClicked
+ }
+ }
+
+ private fun toViewModel(
+ model: UserModel,
+ ): UserViewModel {
+ return UserViewModel(
+ viewKey = model.id,
+ name = model.name,
+ image = model.image,
+ isSelectionMarkerVisible = model.isSelected,
+ alpha =
+ if (model.isSelectable) {
+ LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA
+ } else {
+ LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA
+ },
+ onClicked = createOnSelectedCallback(model),
+ )
+ }
+
+ private fun toViewModel(
+ model: UserActionModel,
+ ): UserActionViewModel {
+ return UserActionViewModel(
+ viewKey = model.ordinal.toLong(),
+ iconResourceId =
+ if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
+ R.drawable.ic_manage_users
+ } else {
+ LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ )
+ },
+ textResourceId =
+ if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
+ R.string.manage_users
+ } else {
+ LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = userInteractor.isGuestUserResetting,
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ )
+ },
+ onClicked = { userInteractor.executeAction(action = model) },
+ )
+ }
+
+ private fun createOnSelectedCallback(model: UserModel): (() -> Unit)? {
+ return if (!model.isSelectable) {
+ null
+ } else {
+ { userInteractor.selectUser(model.id) }
+ }
+ }
+
+ class Factory
+ @Inject
+ constructor(
+ private val userInteractor: UserInteractor,
+ private val powerInteractor: PowerInteractor,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ @Suppress("UNCHECKED_CAST")
+ return UserSwitcherViewModel(
+ userInteractor = userInteractor,
+ powerInteractor = powerInteractor,
+ )
+ as T
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserViewModel.kt
new file mode 100644
index 000000000000..d57bba0fa86a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import com.android.systemui.common.shared.model.Text
+
+/** Models UI state for representing a single user. */
+data class UserViewModel(
+ /**
+ * Key to use with the view or compose system to keep track of the view/composable across
+ * changes to the collection of [UserViewModel] instances.
+ */
+ val viewKey: Int,
+ val name: Text,
+ val image: Drawable,
+ /** Whether a marker should be shown to highlight that this user is the selected one. */
+ val isSelectionMarkerVisible: Boolean,
+ val alpha: Float,
+ val onClicked: (() -> Unit)?,
+)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
new file mode 100644
index 000000000000..ef4500df3600
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserSwitcherViewModelTest : SysuiTestCase() {
+
+ @Mock private lateinit var controller: UserSwitcherController
+ @Mock private lateinit var activityStarter: ActivityStarter
+
+ private lateinit var underTest: UserSwitcherViewModel
+
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var powerRepository: FakePowerRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ userRepository = FakeUserRepository()
+ keyguardRepository = FakeKeyguardRepository()
+ powerRepository = FakePowerRepository()
+ underTest =
+ UserSwitcherViewModel.Factory(
+ userInteractor =
+ UserInteractor(
+ repository = userRepository,
+ controller = controller,
+ activityStarter = activityStarter,
+ keyguardInteractor =
+ KeyguardInteractor(
+ repository = keyguardRepository,
+ )
+ ),
+ powerInteractor =
+ PowerInteractor(
+ repository = powerRepository,
+ ),
+ )
+ .create(UserSwitcherViewModel::class.java)
+ }
+
+ @Test
+ fun users() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setUsers(
+ listOf(
+ UserModel(
+ id = 0,
+ name = Text.Loaded("zero"),
+ image = USER_IMAGE,
+ isSelected = true,
+ isSelectable = true,
+ ),
+ UserModel(
+ id = 1,
+ name = Text.Loaded("one"),
+ image = USER_IMAGE,
+ isSelected = false,
+ isSelectable = true,
+ ),
+ UserModel(
+ id = 2,
+ name = Text.Loaded("two"),
+ image = USER_IMAGE,
+ isSelected = false,
+ isSelectable = false,
+ ),
+ )
+ )
+
+ var userViewModels: List<UserViewModel>? = null
+ val job = underTest.users.onEach { userViewModels = it }.launchIn(this)
+
+ assertThat(userViewModels).hasSize(3)
+ assertUserViewModel(
+ viewModel = userViewModels?.get(0),
+ viewKey = 0,
+ name = "zero",
+ isSelectionMarkerVisible = true,
+ alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
+ isClickable = true,
+ )
+ assertUserViewModel(
+ viewModel = userViewModels?.get(1),
+ viewKey = 1,
+ name = "one",
+ isSelectionMarkerVisible = false,
+ alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
+ isClickable = true,
+ )
+ assertUserViewModel(
+ viewModel = userViewModels?.get(2),
+ viewKey = 2,
+ name = "two",
+ isSelectionMarkerVisible = false,
+ alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA,
+ isClickable = false,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `maximumUserColumns - few users`() =
+ runBlocking(IMMEDIATE) {
+ setUsers(count = 2)
+ var value: Int? = null
+ val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(4)
+ job.cancel()
+ }
+
+ @Test
+ fun `maximumUserColumns - many users`() =
+ runBlocking(IMMEDIATE) {
+ setUsers(count = 5)
+ var value: Int? = null
+ val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(3)
+ job.cancel()
+ }
+
+ @Test
+ fun `isOpenMenuButtonVisible - has actions - true`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+
+ var isVisible: Boolean? = null
+ val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
+
+ assertThat(isVisible).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun `isOpenMenuButtonVisible - no actions - false`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(emptyList())
+
+ var isVisible: Boolean? = null
+ val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
+
+ assertThat(isVisible).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun menu() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+ var isMenuVisible: Boolean? = null
+ val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this)
+ assertThat(isMenuVisible).isFalse()
+
+ underTest.onOpenMenuButtonClicked()
+ assertThat(isMenuVisible).isTrue()
+
+ underTest.onMenuClosed()
+ assertThat(isMenuVisible).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when user is switched`() =
+ runBlocking(IMMEDIATE) {
+ setUsers(count = 2)
+ var isFinishRequested: Boolean? = null
+ val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
+ assertThat(isFinishRequested).isFalse()
+
+ userRepository.setSelectedUser(1)
+ yield()
+ assertThat(isFinishRequested).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when the screen turns off`() =
+ runBlocking(IMMEDIATE) {
+ setUsers(count = 2)
+ powerRepository.setInteractive(true)
+ var isFinishRequested: Boolean? = null
+ val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
+ assertThat(isFinishRequested).isFalse()
+
+ powerRepository.setInteractive(false)
+ yield()
+ assertThat(isFinishRequested).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when cancel button is clicked`() =
+ runBlocking(IMMEDIATE) {
+ setUsers(count = 2)
+ powerRepository.setInteractive(true)
+ var isFinishRequested: Boolean? = null
+ val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
+ assertThat(isFinishRequested).isFalse()
+
+ underTest.onCancelButtonClicked()
+ yield()
+ assertThat(isFinishRequested).isTrue()
+
+ underTest.onFinished()
+ yield()
+ assertThat(isFinishRequested).isFalse()
+
+ job.cancel()
+ }
+
+ private fun setUsers(count: Int) {
+ userRepository.setUsers(
+ (0 until count).map { index ->
+ UserModel(
+ id = index,
+ name = Text.Loaded("$index"),
+ image = USER_IMAGE,
+ isSelected = index == 0,
+ isSelectable = true,
+ )
+ }
+ )
+ }
+
+ private fun assertUserViewModel(
+ viewModel: UserViewModel?,
+ viewKey: Int,
+ name: String,
+ isSelectionMarkerVisible: Boolean,
+ alpha: Float,
+ isClickable: Boolean,
+ ) {
+ checkNotNull(viewModel)
+ assertThat(viewModel.viewKey).isEqualTo(viewKey)
+ assertThat(viewModel.name).isEqualTo(Text.Loaded(name))
+ assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible)
+ assertThat(viewModel.alpha).isEqualTo(alpha)
+ assertThat(viewModel.onClicked != null).isEqualTo(isClickable)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val USER_IMAGE = mock<Drawable>()
+ }
+}