Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e6bdfa55 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Automerger Merge Worker
Browse files

Merge "Cleans up old user switcher impl. and flags." into tm-qpr-dev am: c588abbb am: 8dc22f53

parents ad153f79 8dc22f53
Loading
Loading
Loading
Loading
+0 −392
Original line number Diff line number Diff line
/*
 * 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.compose

import android.graphics.drawable.Drawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.systemui.common.ui.compose.load
import com.android.systemui.compose.SysUiOutlinedButton
import com.android.systemui.compose.SysUiTextButton
import com.android.systemui.compose.features.R
import com.android.systemui.compose.theme.LocalAndroidColorScheme
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 java.lang.Integer.min
import kotlin.math.ceil

@Composable
fun UserSwitcherScreen(
    viewModel: UserSwitcherViewModel,
    onFinished: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false)
    val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList())
    val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1)
    val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList())
    val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false)
    val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false)

    UserSwitcherScreenStateless(
        isFinishRequested = isFinishRequested,
        users = users,
        maxUserColumns = maxUserColumns,
        menuActions = menuActions,
        isOpenMenuButtonVisible = isOpenMenuButtonVisible,
        isMenuVisible = isMenuVisible,
        onMenuClosed = viewModel::onMenuClosed,
        onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked,
        onCancelButtonClicked = viewModel::onCancelButtonClicked,
        onFinished = {
            onFinished()
            viewModel.onFinished()
        },
        modifier = modifier,
    )
}

@Composable
private fun UserSwitcherScreenStateless(
    isFinishRequested: Boolean,
    users: List<UserViewModel>,
    maxUserColumns: Int,
    menuActions: List<UserActionViewModel>,
    isOpenMenuButtonVisible: Boolean,
    isMenuVisible: Boolean,
    onMenuClosed: () -> Unit,
    onOpenMenuButtonClicked: () -> Unit,
    onCancelButtonClicked: () -> Unit,
    onFinished: () -> Unit,
    modifier: Modifier = Modifier,
) {
    LaunchedEffect(isFinishRequested) {
        if (isFinishRequested) {
            onFinished()
        }
    }

    Box(
        modifier =
            modifier
                .fillMaxSize()
                .padding(
                    horizontal = 60.dp,
                    vertical = 40.dp,
                ),
    ) {
        UserGrid(
            users = users,
            maxUserColumns = maxUserColumns,
            modifier = Modifier.align(Alignment.Center),
        )

        Buttons(
            menuActions = menuActions,
            isOpenMenuButtonVisible = isOpenMenuButtonVisible,
            isMenuVisible = isMenuVisible,
            onMenuClosed = onMenuClosed,
            onOpenMenuButtonClicked = onOpenMenuButtonClicked,
            onCancelButtonClicked = onCancelButtonClicked,
            modifier = Modifier.align(Alignment.BottomEnd),
        )
    }
}

@Composable
private fun UserGrid(
    users: List<UserViewModel>,
    maxUserColumns: Int,
    modifier: Modifier = Modifier,
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(44.dp),
        modifier = modifier,
    ) {
        val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt()
        (0 until rowCount).forEach { rowIndex ->
            Row(
                horizontalArrangement = Arrangement.spacedBy(64.dp),
                modifier = modifier,
            ) {
                val fromIndex = rowIndex * maxUserColumns
                val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns)
                users.subList(fromIndex, toIndex).forEach { user ->
                    UserItem(
                        viewModel = user,
                    )
                }
            }
        }
    }
}

@Composable
private fun UserItem(
    viewModel: UserViewModel,
) {
    val onClicked = viewModel.onClicked
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier =
            if (onClicked != null) {
                    Modifier.clickable { onClicked() }
                } else {
                    Modifier
                }
                .alpha(viewModel.alpha),
    ) {
        Box {
            UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp))

            UserItemIcon(
                image = viewModel.image,
                isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible,
                modifier = Modifier.align(Alignment.Center).size(222.dp)
            )
        }

        // User name
        val text = viewModel.name.load()
        if (text != null) {
            // We use the box to center-align the text vertically as that is not possible with Text
            // alone.
            Box(
                modifier = Modifier.size(width = 222.dp, height = 48.dp),
            ) {
                Text(
                    text = text,
                    style = MaterialTheme.typography.titleLarge,
                    color = colorResource(com.android.internal.R.color.system_neutral1_50),
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.align(Alignment.Center),
                )
            }
        }
    }
}

@Composable
private fun UserItemBackground(
    modifier: Modifier = Modifier,
) {
    Image(
        painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground),
        contentDescription = null,
        modifier = modifier.clip(CircleShape),
    )
}

@Composable
private fun UserItemIcon(
    image: Drawable,
    isSelectionMarkerVisible: Boolean,
    modifier: Modifier = Modifier,
) {
    Image(
        bitmap = image.toBitmap().asImageBitmap(),
        contentDescription = null,
        modifier =
            if (isSelectionMarkerVisible) {
                    // Draws a ring
                    modifier.border(
                        width = 8.dp,
                        color = LocalAndroidColorScheme.current.colorAccentPrimary,
                        shape = CircleShape,
                    )
                } else {
                    modifier
                }
                .padding(16.dp)
                .clip(CircleShape)
    )
}

@Composable
private fun Buttons(
    menuActions: List<UserActionViewModel>,
    isOpenMenuButtonVisible: Boolean,
    isMenuVisible: Boolean,
    onMenuClosed: () -> Unit,
    onOpenMenuButtonClicked: () -> Unit,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(
        modifier = modifier,
    ) {
        // Cancel button.
        SysUiTextButton(
            onClick = onCancelButtonClicked,
        ) {
            Text(stringResource(R.string.cancel))
        }

        // "Open menu" button.
        if (isOpenMenuButtonVisible) {
            Spacer(modifier = Modifier.width(8.dp))
            // To properly use a DropdownMenu in Compose, we need to wrap the button that opens it
            // and the menu itself in a Box.
            Box {
                SysUiOutlinedButton(
                    onClick = onOpenMenuButtonClicked,
                ) {
                    Text(stringResource(R.string.add))
                }
                Menu(
                    viewModel = menuActions,
                    isMenuVisible = isMenuVisible,
                    onMenuClosed = onMenuClosed,
                )
            }
        }
    }
}

@Composable
private fun Menu(
    viewModel: List<UserActionViewModel>,
    isMenuVisible: Boolean,
    onMenuClosed: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4
    DropdownMenu(
        expanded = isMenuVisible,
        onDismissRequest = onMenuClosed,
        modifier =
            modifier.background(
                color = MaterialTheme.colorScheme.inverseOnSurface,
            ),
    ) {
        viewModel.forEachIndexed { index, action ->
            MenuItem(
                viewModel = action,
                onClicked = { action.onClicked() },
                topPadding =
                    if (index == 0) {
                        16.dp
                    } else {
                        0.dp
                    },
                bottomPadding =
                    if (index == viewModel.size - 1) {
                        16.dp
                    } else {
                        0.dp
                    },
                modifier = Modifier.sizeIn(maxWidth = maxItemWidth),
            )
        }
    }
}

@Composable
private fun MenuItem(
    viewModel: UserActionViewModel,
    onClicked: () -> Unit,
    topPadding: Dp,
    bottomPadding: Dp,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val density = LocalDensity.current

    val icon =
        remember(viewModel.iconResourceId) {
            val drawable =
                checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
            val size = with(density) { 20.dp.toPx() }.toInt()
            drawable
                .toBitmap(
                    width = size,
                    height = size,
                )
                .asImageBitmap()
        }

    DropdownMenuItem(
        text = {
            Text(
                text = stringResource(viewModel.textResourceId),
                style = MaterialTheme.typography.bodyMedium,
            )
        },
        onClick = onClicked,
        leadingIcon = {
            Spacer(modifier = Modifier.width(10.dp))
            Image(
                bitmap = icon,
                contentDescription = null,
            )
        },
        modifier =
            modifier
                .heightIn(
                    min = 56.dp,
                )
                .padding(
                    start = 18.dp,
                    end = 65.dp,
                    top = topPadding,
                    bottom = bottomPadding,
                ),
    )
}
+0 −22
Original line number Diff line number Diff line
@@ -114,28 +114,6 @@ object Flags {
    // TODO(b/254512385): Tracking Bug
    @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")

    /**
     * Whether the user interactor and repository should use `UserSwitcherController`.
     *
     * If this is `false`, the interactor and repo skip the controller and directly access the
     * framework APIs.
     */
    // TODO(b/254513286): Tracking Bug
    val USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
        unreleasedFlag(210, "user_interactor_and_repo_use_controller")

    /**
     * Whether `UserSwitcherController` should use the user interactor.
     *
     * When this is `true`, the controller does not directly access framework APIs. Instead, it goes
     * through the interactor.
     *
     * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it
     * would created a cycle between controller -> interactor -> controller.
     */
    // TODO(b/254513102): Tracking Bug
    val USER_CONTROLLER_USES_INTERACTOR = releasedFlag(211, "user_controller_uses_interactor")

    /**
     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
     * the digits when the clock moves.
+0 −2
Original line number Diff line number Diff line
@@ -1131,7 +1131,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
        // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
        mNotificationIconAreaController.setupShelf(mNotificationShelfController);
        mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
        mUserSwitcherController.init(mNotificationShadeWindowView);

        // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
        mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
@@ -4266,7 +4265,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
            }
            // TODO: Bring these out of CentralSurfaces.
            mUserInfoControllerImpl.onDensityOrFontScaleChanged();
            mUserSwitcherController.onDensityOrFontScaleChanged();
            mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
            mHeadsUpManager.onDensityOrFontScaleChanged();
        }
+1 −2
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import android.graphics.ColorFilter
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.widget.BaseAdapter
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
@@ -84,7 +83,7 @@ protected constructor(
    }

    fun refresh() {
        controller.refreshUsers(UserHandle.USER_NULL)
        controller.refreshUsers()
    }

    companion object {
+108 −43
Original line number Diff line number Diff line
@@ -14,35 +14,74 @@
 * limitations under the License.
 *
 */

package com.android.systemui.statusbar.policy

import android.annotation.UserIdInt
import android.content.Context
import android.content.Intent
import android.view.View
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

/** Access point into multi-user switching logic. */
@Deprecated("Use UserInteractor or GuestUserInteractor instead.")
@SysUISingleton
class UserSwitcherController
@Inject
constructor(
    @Application private val applicationContext: Context,
    private val userInteractorLazy: Lazy<UserInteractor>,
    private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
    private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
    private val activityStarter: ActivityStarter,
) {

/** Defines interface for a class that provides user switching functionality and state. */
interface UserSwitcherController : Dumpable {
    /** Defines interface for classes that can be called back when the user is switched. */
    fun interface UserSwitchCallback {
        /** Notifies that the user has switched. */
        fun onUserSwitched()
    }

    private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
    private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
    private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }

    private val callbackCompatMap = mutableMapOf<UserSwitchCallback, UserInteractor.UserCallback>()

    /** The current list of [UserRecord]. */
    val users: ArrayList<UserRecord>
        get() = userInteractor.userRecords.value

    /** Whether the user switcher experience should use the simple experience. */
    val isSimpleUserSwitcher: Boolean

    /** Require a view for jank detection */
    fun init(view: View)
        get() = userInteractor.isSimpleUserSwitcher

    /** The [UserRecord] of the current user or `null` when none. */
    val currentUserRecord: UserRecord?
        get() = userInteractor.selectedUserRecord.value

    /** The name of the current user of the device or `null`, when none is selected. */
    val currentUserName: String?
        get() =
            currentUserRecord?.let {
                LegacyUserUiHelper.getUserRecordName(
                    context = applicationContext,
                    record = it,
                    isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
                    isGuestUserResetting = userInteractor.isGuestUserResetting,
                )
            }

    /**
     * Notifies that a user has been selected.
@@ -55,34 +94,40 @@ interface UserSwitcherController : Dumpable {
     * @param userId The ID of the user to switch to.
     * @param dialogShower An optional [DialogShower] in case we need to show dialogs.
     */
    fun onUserSelected(userId: Int, dialogShower: DialogShower?)

    /** Whether it is allowed to add users while the device is locked. */
    val isAddUsersFromLockScreenEnabled: Flow<Boolean>
    fun onUserSelected(userId: Int, dialogShower: DialogShower?) {
        userInteractor.selectUser(userId, dialogShower)
    }

    /** Whether the guest user is configured to always be present on the device. */
    val isGuestUserAutoCreated: Boolean
        get() = userInteractor.isGuestUserAutoCreated

    /** Whether the guest user is currently being reset. */
    val isGuestUserResetting: Boolean

    /** Creates and switches to the guest user. */
    fun createAndSwitchToGuestUser(dialogShower: DialogShower?)

    /** Shows the add user dialog. */
    fun showAddUserDialog(dialogShower: DialogShower?)

    /** Starts an activity to add a supervised user to the device. */
    fun startSupervisedUserActivity()

    /** Notifies when the display density or font scale has changed. */
    fun onDensityOrFontScaleChanged()
        get() = userInteractor.isGuestUserResetting

    /** Registers an adapter to notify when the users change. */
    fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>)
    fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
        userInteractor.addCallback(
            object : UserInteractor.UserCallback {
                override fun isEvictable(): Boolean {
                    return adapter.get() == null
                }

                override fun onUserStateChanged() {
                    adapter.get()?.notifyDataSetChanged()
                }
            }
        )
    }

    /** Notifies the item for a user has been clicked. */
    fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?)
    fun onUserListItemClicked(
        record: UserRecord,
        dialogShower: DialogShower?,
    ) {
        userInteractor.onRecordSelected(record, dialogShower)
    }

    /**
     * Removes guest user and switches to target user. The guest must be the current user and its id
@@ -103,7 +148,12 @@ interface UserSwitcherController : Dumpable {
     * @param targetUserId id of the user to switch to after guest is removed. If
     * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user.
     */
    fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int)
    fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
        userInteractor.removeGuestUser(
            guestUserId = guestUserId,
            targetUserId = targetUserId,
        )
    }

    /**
     * Exits guest user and switches to previous non-guest user. The guest must be the current user.
@@ -114,43 +164,58 @@ interface UserSwitcherController : Dumpable {
     * @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest
     * only if its ephemeral, else keep guest
     */
    fun exitGuestUser(
        @UserIdInt guestUserId: Int,
        @UserIdInt targetUserId: Int,
        forceRemoveGuestOnExit: Boolean
    )
    fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) {
        userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
    }

    /**
     * Guarantee guest is present only if the device is provisioned. Otherwise, create a content
     * observer to wait until the device is provisioned, then schedule the guest creation.
     */
    fun schedulePostBootGuestCreation()
    fun schedulePostBootGuestCreation() {
        guestUserInteractor.onDeviceBootCompleted()
    }

    /** Whether keyguard is showing. */
    val isKeyguardShowing: Boolean
        get() = keyguardInteractor.isKeyguardShowing()

    /** Starts an activity with the given [Intent]. */
    fun startActivity(intent: Intent)
    fun startActivity(intent: Intent) {
        activityStarter.startActivity(intent, /* dismissShade= */ true)
    }

    /**
     * Refreshes users from UserManager.
     *
     * The pictures are only loaded if they have not been loaded yet.
     *
     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
     */
    fun refreshUsers(forcePictureLoadForId: Int)
    fun refreshUsers() {
        userInteractor.refreshUsers()
    }

    /** Adds a subscriber to when user switches. */
    fun addUserSwitchCallback(callback: UserSwitchCallback)
    fun addUserSwitchCallback(callback: UserSwitchCallback) {
        val interactorCallback =
            object : UserInteractor.UserCallback {
                override fun onUserStateChanged() {
                    callback.onUserSwitched()
                }
            }
        callbackCompatMap[callback] = interactorCallback
        userInteractor.addCallback(interactorCallback)
    }

    /** Removes a previously-added subscriber. */
    fun removeUserSwitchCallback(callback: UserSwitchCallback)
    fun removeUserSwitchCallback(callback: UserSwitchCallback) {
        val interactorCallback = callbackCompatMap.remove(callback)
        if (interactorCallback != null) {
            userInteractor.removeCallback(interactorCallback)
        }
    }

    /** Defines interface for classes that can be called back when the user is switched. */
    fun interface UserSwitchCallback {
        /** Notifies that the user has switched. */
        fun onUserSwitched()
    fun dump(pw: PrintWriter, args: Array<out String>) {
        userInteractor.dump(pw)
    }

    companion object {
Loading