User UI layer.
UserSwitcherActivity refactor: CL 6/7 Includes view-models and view-binders for the user and user-switcher experiences. Bug: 243844359 Test: Integration tests included for the top-most view-model. Also manually tested with the next CL. Change-Id: Ia05c2f267a37475c409ffed9b9784cdc435a9d76
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)?,
|
||||
)
|
||||
@@ -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>()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user