/* * 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 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.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserSwitcherInteractor 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 kotlin.math.ceil 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. */ @SysUISingleton class UserSwitcherViewModel @Inject constructor( private val userSwitcherInteractor: UserSwitcherInteractor, private val guestUserInteractor: GuestUserInteractor, ) { /** The currently selected user. */ val selectedUser: Flow = userSwitcherInteractor.selectedUser.map { user -> toViewModel(user) } /** On-device users. */ val users: Flow> = userSwitcherInteractor.users.map { models -> models.map { user -> toViewModel(user) } } /** The maximum number of columns that the user selection grid should use. */ val maximumUserColumns: Flow = users.map { getMaxUserSwitcherItemColumns(it.size) } 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 = _isMenuVisible /** The user action menu. */ val menu: Flow> = userSwitcherInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } /** Whether the button to open the user action menu is visible. */ val isOpenMenuButtonVisible: Flow = menu.map { it.isNotEmpty() } private val hasCancelButtonBeenClicked = MutableStateFlow(false) private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) private val userSwitched = MutableStateFlow(false) /** * Whether the observer should finish the experience. Once consumed, [onFinished] must be called * by the consumer. */ val isFinishRequested: Flow = 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 isFinishRequiredDueToExecutedAction.value = false userSwitched.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 } /** Returns the maximum number of columns for user items in the user switcher. */ private fun getMaxUserSwitcherItemColumns(userCount: Int): Int { return if (userCount < 5) { 4 } else { ceil(userCount / 2.0).toInt() } } private fun createFinishRequestedFlow(): Flow = combine( // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, // If an executed action told us to finish, we should finish, isFinishRequiredDueToExecutedAction, userSwitched, ) { cancelButtonClicked, executedActionFinish, userSwitched -> cancelButtonClicked || executedActionFinish || userSwitched } private fun toViewModel( model: UserModel, ): UserViewModel { return UserViewModel( viewKey = model.id, name = if (model.isGuest && model.isSelected) { Text.Resource(com.android.settingslib.R.string.guest_exit_quick_settings_button) } else { model.name }, image = CircularDrawable(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 = LegacyUserUiHelper.getUserSwitcherActionIconResourceId( isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddUser = model == UserActionModel.ADD_USER, isGuest = model == UserActionModel.ENTER_GUEST_MODE, isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, isTablet = true, ), textResourceId = LegacyUserUiHelper.getUserSwitcherActionTextResourceId( isGuest = model == UserActionModel.ENTER_GUEST_MODE, isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, isGuestUserResetting = guestUserInteractor.isGuestUserResetting, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddUser = model == UserActionModel.ADD_USER, isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, isTablet = true, ), onClicked = { userSwitcherInteractor.executeAction(action = model) // We don't finish because we want to show a dialog over the full-screen UI and // that dialog can be dismissed in case the user changes their mind and decides not // to add a user. // // We finish for all other actions because they navigate us away from the // full-screen experience or are destructive (like changing to the guest user). val shouldFinish = model != UserActionModel.ADD_USER if (shouldFinish) { isFinishRequiredDueToExecutedAction.value = true } }, ) } private fun createOnSelectedCallback(model: UserModel): (() -> Unit)? { return if (!model.isSelectable) { null } else { { userSwitcherInteractor.selectUser(model.id) userSwitched.value = true } } } }