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

Commit 7c822664 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

GuestUserInteractor.

Business logic surrounding guest user.

Bug: 246631653
Test: included in CL
Change-Id: I7363f64f93fff803ecabd3db4050fcdc9981aaaf
parent 0fd4ee12
Loading
Loading
Loading
Loading
+322 −0
Original line number Original line 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.domain.interactor

import android.annotation.UserIdInt
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.pm.UserInfo
import android.os.RemoteException
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import android.view.WindowManagerGlobal
import android.widget.Toast
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext

/** Encapsulates business logic to interact with guest user data and systems. */
@SysUISingleton
class GuestUserInteractor
@Inject
constructor(
    @Application private val applicationContext: Context,
    @Application private val applicationScope: CoroutineScope,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val manager: UserManager,
    private val repository: UserRepository,
    private val deviceProvisionedController: DeviceProvisionedController,
    private val devicePolicyManager: DevicePolicyManager,
    private val refreshUsersScheduler: RefreshUsersScheduler,
    private val uiEventLogger: UiEventLogger,
) {
    /** Whether the device is configured to always have a guest user available. */
    val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated

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

    /** Notifies that the device has finished booting. */
    fun onDeviceBootCompleted() {
        applicationScope.launch {
            if (isDeviceAllowedToAddGuest()) {
                guaranteePresent()
                return@launch
            }

            suspendCancellableCoroutine<Unit> { continuation ->
                val callback =
                    object : DeviceProvisionedController.DeviceProvisionedListener {
                        override fun onDeviceProvisionedChanged() {
                            continuation.resumeWith(Result.success(Unit))
                            deviceProvisionedController.removeCallback(this)
                        }
                    }

                deviceProvisionedController.addCallback(callback)
            }

            if (isDeviceAllowedToAddGuest()) {
                guaranteePresent()
            }
        }
    }

    /** Creates a guest user and switches to it. */
    fun createAndSwitchTo(
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        selectUser: (userId: Int) -> Unit,
    ) {
        applicationScope.launch {
            val newGuestUserId = create(showDialog, dismissDialog)
            if (newGuestUserId != UserHandle.USER_NULL) {
                selectUser(newGuestUserId)
            }
        }
    }

    /** Exits the guest user, switching back to the last non-guest user or to the default user. */
    fun exit(
        @UserIdInt guestUserId: Int,
        @UserIdInt targetUserId: Int,
        forceRemoveGuestOnExit: Boolean,
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        switchUser: (userId: Int) -> Unit,
    ) {
        val currentUserInfo = repository.getSelectedUserInfo()
        if (currentUserInfo.id != guestUserId) {
            Log.w(
                TAG,
                "User requesting to start a new session ($guestUserId) is not current user" +
                    " (${currentUserInfo.id})"
            )
            return
        }

        if (!currentUserInfo.isGuest) {
            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
            return
        }

        applicationScope.launch {
            var newUserId = UserHandle.USER_SYSTEM
            if (targetUserId == UserHandle.USER_NULL) {
                // When a target user is not specified switch to last non guest user:
                val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
                if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
                    val info =
                        withContext(backgroundDispatcher) {
                            manager.getUserInfo(lastSelectedNonGuestUserHandle)
                        }
                    if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
                        newUserId = info.id
                    }
                }
            } else {
                newUserId = targetUserId
            }

            if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
                remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
            } else {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
                switchUser(newUserId)
            }
        }
    }

    /**
     * Guarantees that the guest user is present on the device, creating it if needed and if allowed
     * to.
     */
    suspend fun guaranteePresent() {
        if (!isDeviceAllowedToAddGuest()) {
            return
        }

        val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
        if (guestUser == null) {
            scheduleCreation()
        }
    }

    /** Removes the guest user from the device. */
    private suspend fun remove(
        @UserIdInt guestUserId: Int,
        @UserIdInt targetUserId: Int,
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
        switchUser: (userId: Int) -> Unit,
    ) {
        val currentUser: UserInfo = repository.getSelectedUserInfo()
        if (currentUser.id != guestUserId) {
            Log.w(
                TAG,
                "User requesting to start a new session ($guestUserId) is not current user" +
                    " ($currentUser.id)"
            )
            return
        }

        if (!currentUser.isGuest) {
            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
            return
        }

        val marked =
            withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
        if (!marked) {
            Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
            return
        }

        if (targetUserId == UserHandle.USER_NULL) {
            // Create a new guest in the foreground, and then immediately switch to it
            val newGuestId = create(showDialog, dismissDialog)
            if (newGuestId == UserHandle.USER_NULL) {
                Log.e(TAG, "Could not create new guest, switching back to system user")
                switchUser(UserHandle.USER_SYSTEM)
                withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
                try {
                    WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
                } catch (e: RemoteException) {
                    Log.e(
                        TAG,
                        "Couldn't remove guest because ActivityManager or WindowManager is dead"
                    )
                }
                return
            }

            switchUser(newGuestId)

            withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
        } else {
            if (repository.isGuestUserAutoCreated) {
                repository.isGuestUserResetting = true
            }
            switchUser(targetUserId)
            manager.removeUser(currentUser.id)
        }
    }

    /**
     * Creates the guest user and adds it to the device.
     *
     * @param showDialog A function to invoke to show a dialog.
     * @param dismissDialog A function to invoke to dismiss a dialog.
     * @return The user ID of the newly-created guest user.
     */
    private suspend fun create(
        showDialog: (ShowDialogRequestModel) -> Unit,
        dismissDialog: () -> Unit,
    ): Int {
        return withContext(mainDispatcher) {
            showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
            val guestUserId = createInBackground()
            dismissDialog()
            if (guestUserId != UserHandle.USER_NULL) {
                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
            } else {
                Toast.makeText(
                        applicationContext,
                        com.android.settingslib.R.string.add_guest_failed,
                        Toast.LENGTH_SHORT,
                    )
                    .show()
            }

            guestUserId
        }
    }

    /** Schedules the creation of the guest user. */
    private suspend fun scheduleCreation() {
        if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
            return
        }

        withContext(backgroundDispatcher) {
            val newGuestUserId = createInBackground()
            repository.isGuestUserCreationScheduled.set(false)
            repository.isGuestUserResetting = false
            if (newGuestUserId == UserHandle.USER_NULL) {
                Log.w(TAG, "Could not create new guest while exiting existing guest")
                // Refresh users so that we still display "Guest" if
                // config_guestUserAutoCreated=true
                refreshUsersScheduler.refreshIfNotPaused()
            }
        }
    }

    /**
     * Creates a guest user and return its multi-user user ID.
     *
     * This method does not check if a guest already exists before it makes a call to [UserManager]
     * to create a new one.
     *
     * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
     * the guest couldn't be created.
     */
    @UserIdInt
    private suspend fun createInBackground(): Int {
        return withContext(backgroundDispatcher) {
            try {
                val guestUser = manager.createGuest(applicationContext)
                if (guestUser != null) {
                    guestUser.id
                } else {
                    Log.e(
                        TAG,
                        "Couldn't create guest, most likely because there already exists one!"
                    )
                    UserHandle.USER_NULL
                }
            } catch (e: UserManager.UserOperationException) {
                Log.e(TAG, "Couldn't create guest user!", e)
                UserHandle.USER_NULL
            }
        }
    }

    private fun isDeviceAllowedToAddGuest(): Boolean {
        return deviceProvisionedController.isDeviceProvisioned &&
            !devicePolicyManager.isDeviceManaged
    }

    companion object {
        private const val TAG = "GuestUserInteractor"
    }
}
+41 −0
Original line number Original line 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.domain.model

import android.os.UserHandle

/** Encapsulates a request to show a dialog. */
sealed class ShowDialogRequestModel {
    data class ShowAddUserDialog(
        val userHandle: UserHandle,
        val isKeyguardShowing: Boolean,
        val showEphemeralMessage: Boolean,
    ) : ShowDialogRequestModel()

    data class ShowUserCreationDialog(
        val isGuest: Boolean,
    ) : ShowDialogRequestModel()

    data class ShowExitGuestDialog(
        val guestUserId: Int,
        val targetUserId: Int,
        val isGuestEphemeral: Boolean,
        val isKeyguardShowing: Boolean,
        val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
    ) : ShowDialogRequestModel()
}
+336 −0

File added.

Preview size limit exceeded, changes collapsed.