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

Commit 32b5486a authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

User data layer without dependency on USC

Behind a flag, removes the dependency on UserSwitcherController from the
data layer of the user feature.

Bug: 246631653
Test: included tests, manually verified against the tip of the CL chain
Change-Id: I0594dcfd5cc38d0ae09f620e01d0caafc1b956e3
parent 59ad257e
Loading
Loading
Loading
Loading
+25 −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.data.model

/** Encapsulates the state of settings related to user switching. */
data class UserSwitcherSettingsModel(
    val isSimpleUserSwitcher: Boolean = false,
    val isAddUsersFromLockscreen: Boolean = false,
    val isUserSwitcherEnabled: Boolean = false,
)
+219 −5
Original line number Original line Diff line number Diff line
@@ -18,9 +18,13 @@
package com.android.systemui.user.data.repository
package com.android.systemui.user.data.repository


import android.content.Context
import android.content.Context
import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager
import android.os.UserManager
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons
import com.android.internal.util.UserIcons
import com.android.systemui.R
import com.android.systemui.R
@@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.common.shared.model.Text
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
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.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext


/**
/**
 * Acts as source of truth for user related data.
 * Acts as source of truth for user related data.
@@ -55,6 +80,18 @@ interface UserRepository {
    /** List of available user-related actions. */
    /** List of available user-related actions. */
    val actions: Flow<List<UserActionModel>>
    val actions: Flow<List<UserActionModel>>


    /** User switcher related settings. */
    val userSwitcherSettings: Flow<UserSwitcherSettingsModel>

    /** List of all users on the device. */
    val userInfos: Flow<List<UserInfo>>

    /** [UserInfo] of the currently-selected user. */
    val selectedUserInfo: Flow<UserInfo>

    /** User ID of the last non-guest selected user. */
    val lastSelectedNonGuestUserId: Int

    /** Whether actions are available even when locked. */
    /** Whether actions are available even when locked. */
    val isActionableWhenLocked: Flow<Boolean>
    val isActionableWhenLocked: Flow<Boolean>


@@ -62,7 +99,23 @@ interface UserRepository {
    val isGuestUserAutoCreated: Boolean
    val isGuestUserAutoCreated: Boolean


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

    /** Whether we've scheduled the creation of a guest user. */
    val isGuestUserCreationScheduled: AtomicBoolean

    /** The user of the secondary service. */
    var secondaryUserId: Int

    /** Whether refresh users should be paused. */
    var isRefreshUsersPaused: Boolean

    /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
    fun refreshUsers()

    fun getSelectedUserInfo(): UserInfo

    fun isSimpleUserSwitcher(): Boolean
}
}


@SysUISingleton
@SysUISingleton
@@ -71,9 +124,31 @@ class UserRepositoryImpl
constructor(
constructor(
    @Application private val appContext: Context,
    @Application private val appContext: Context,
    private val manager: UserManager,
    private val manager: UserManager,
    controller: UserSwitcherController,
    private val controller: UserSwitcherController,
    @Application private val applicationScope: CoroutineScope,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val globalSettings: GlobalSettings,
    private val tracker: UserTracker,
    private val featureFlags: FeatureFlags,
) : UserRepository {
) : UserRepository {


    private val isNewImpl: Boolean
        get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)

    private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
    override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
        _userSwitcherSettings.asStateFlow().filterNotNull()

    private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
    override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()

    private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
    override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()

    override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
        private set

    private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
    private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
        fun send() {
        fun send() {
            trySendWithFailureLogging(
            trySendWithFailureLogging(
@@ -99,11 +174,148 @@ constructor(
    override val actions: Flow<List<UserActionModel>> =
    override val actions: Flow<List<UserActionModel>> =
        userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
        userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }


    override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
    override val isActionableWhenLocked: Flow<Boolean> =
        if (isNewImpl) {
            emptyFlow()
        } else {
            controller.isAddUsersFromLockScreenEnabled
        }

    override val isGuestUserAutoCreated: Boolean =
        if (isNewImpl) {
            appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
        } else {
            controller.isGuestUserAutoCreated
        }

    private var _isGuestUserResetting: Boolean = false
    override var isGuestUserResetting: Boolean =
        if (isNewImpl) {
            _isGuestUserResetting
        } else {
            controller.isGuestUserResetting
        }
        set(value) =
            if (isNewImpl) {
                _isGuestUserResetting = value
            } else {
                error("Not supported in the old implementation!")
            }

    override val isGuestUserCreationScheduled = AtomicBoolean()

    override var secondaryUserId: Int = UserHandle.USER_NULL

    override var isRefreshUsersPaused: Boolean = false

    init {
        if (isNewImpl) {
            observeSelectedUser()
            observeUserSettings()
        }
    }

    override fun refreshUsers() {
        applicationScope.launch {
            val result = withContext(backgroundDispatcher) { manager.aliveUsers }


    override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
            if (result != null) {
                _userInfos.value = result
            }
        }
    }

    override fun getSelectedUserInfo(): UserInfo {
        return checkNotNull(_selectedUserInfo.value)
    }


    override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
    override fun isSimpleUserSwitcher(): Boolean {
        return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
    }

    private fun observeSelectedUser() {
        conflatedCallbackFlow {
                fun send() {
                    trySendWithFailureLogging(tracker.userInfo, TAG)
                }

                val callback =
                    object : UserTracker.Callback {
                        override fun onUserChanged(newUser: Int, userContext: Context) {
                            send()
                        }
                    }

                tracker.addCallback(callback, mainDispatcher.asExecutor())
                send()

                awaitClose { tracker.removeCallback(callback) }
            }
            .onEach {
                if (!it.isGuest) {
                    lastSelectedNonGuestUserId = it.id
                }

                _selectedUserInfo.value = it
            }
            .launchIn(applicationScope)
    }

    private fun observeUserSettings() {
        globalSettings
            .observerFlow(
                names =
                    arrayOf(
                        SETTING_SIMPLE_USER_SWITCHER,
                        Settings.Global.ADD_USERS_WHEN_LOCKED,
                        Settings.Global.USER_SWITCHER_ENABLED,
                    ),
                userId = UserHandle.USER_SYSTEM,
            )
            .onStart { emit(Unit) } // Forces an initial update.
            .map { getSettings() }
            .onEach { _userSwitcherSettings.value = it }
            .launchIn(applicationScope)
    }

    private suspend fun getSettings(): UserSwitcherSettingsModel {
        return withContext(backgroundDispatcher) {
            val isSimpleUserSwitcher =
                globalSettings.getIntForUser(
                    SETTING_SIMPLE_USER_SWITCHER,
                    if (
                        appContext.resources.getBoolean(
                            com.android.internal.R.bool.config_expandLockScreenUserSwitcher
                        )
                    ) {
                        1
                    } else {
                        0
                    },
                    UserHandle.USER_SYSTEM,
                ) != 0

            val isAddUsersFromLockscreen =
                globalSettings.getIntForUser(
                    Settings.Global.ADD_USERS_WHEN_LOCKED,
                    0,
                    UserHandle.USER_SYSTEM,
                ) != 0

            val isUserSwitcherEnabled =
                globalSettings.getIntForUser(
                    Settings.Global.USER_SWITCHER_ENABLED,
                    0,
                    UserHandle.USER_SYSTEM,
                ) != 0

            UserSwitcherSettingsModel(
                isSimpleUserSwitcher = isSimpleUserSwitcher,
                isAddUsersFromLockscreen = isAddUsersFromLockscreen,
                isUserSwitcherEnabled = isUserSwitcherEnabled,
            )
        }
    }


    private fun UserRecord.isUser(): Boolean {
    private fun UserRecord.isUser(): Boolean {
        return when {
        return when {
@@ -125,6 +337,7 @@ constructor(
            image = getUserImage(this),
            image = getUserImage(this),
            isSelected = isCurrent,
            isSelected = isCurrent,
            isSelectable = isSwitchToEnabled || isGuest,
            isSelectable = isSwitchToEnabled || isGuest,
            isGuest = isGuest,
        )
        )
    }
    }


@@ -162,5 +375,6 @@ constructor(


    companion object {
    companion object {
        private const val TAG = "UserRepository"
        private const val TAG = "UserRepository"
        @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
    }
    }
}
}
+3 −0
Original line number Original line Diff line number Diff line
@@ -32,4 +32,7 @@ data class UserModel(
    val isSelected: Boolean,
    val isSelected: Boolean,
    /** Whether this use is selectable. A non-selectable user cannot be switched to. */
    /** Whether this use is selectable. A non-selectable user cannot be switched to. */
    val isSelectable: Boolean,
    val isSelectable: Boolean,
    /** Whether this model represents the guest user. */
    // TODO(b/246631653): remove this default value it was only here to be able to split up CLs
    val isGuest: Boolean = false,
)
)
+48 −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.util.settings

import android.annotation.UserIdInt
import android.database.ContentObserver
import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow

/** Kotlin extension functions for [SettingsProxy]. */
object SettingsProxyExt {

    /** Returns a flow of [Unit] that is invoked each time that content is updated. */
    fun SettingsProxy.observerFlow(
        vararg names: String,
        @UserIdInt userId: Int = UserHandle.USER_CURRENT,
    ): Flow<Unit> {
        return conflatedCallbackFlow {
            val observer =
                object : ContentObserver(null) {
                    override fun onChange(selfChange: Boolean) {
                        trySend(Unit)
                    }
                }

            names.forEach { name -> registerContentObserverForUser(name, observer, userId) }

            awaitClose { unregisterContentObserver(observer) }
        }
    }
}
+204 −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.data.repository

import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.`when` as whenever

@SmallTest
@RunWith(JUnit4::class)
class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {

    @Before
    fun setUp() {
        super.setUp(isRefactored = true)
    }

    @Test
    fun userSwitcherSettings() = runSelfCancelingTest {
        setUpGlobalSettings(
            isSimpleUserSwitcher = true,
            isAddUsersFromLockscreen = true,
            isUserSwitcherEnabled = true,
        )
        underTest = create(this)

        var value: UserSwitcherSettingsModel? = null
        underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)

        assertUserSwitcherSettings(
            model = value,
            expectedSimpleUserSwitcher = true,
            expectedAddUsersFromLockscreen = true,
            expectedUserSwitcherEnabled = true,
        )

        setUpGlobalSettings(
            isSimpleUserSwitcher = false,
            isAddUsersFromLockscreen = true,
            isUserSwitcherEnabled = true,
        )
        assertUserSwitcherSettings(
            model = value,
            expectedSimpleUserSwitcher = false,
            expectedAddUsersFromLockscreen = true,
            expectedUserSwitcherEnabled = true,
        )
    }

    @Test
    fun refreshUsers() = runSelfCancelingTest {
        underTest = create(this)
        val initialExpectedValue =
            setUpUsers(
                count = 3,
                selectedIndex = 0,
            )
        var userInfos: List<UserInfo>? = null
        var selectedUserInfo: UserInfo? = null
        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)

        underTest.refreshUsers()
        assertThat(userInfos).isEqualTo(initialExpectedValue)
        assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)

        val secondExpectedValue =
            setUpUsers(
                count = 4,
                selectedIndex = 1,
            )
        underTest.refreshUsers()
        assertThat(userInfos).isEqualTo(secondExpectedValue)
        assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)

        val selectedNonGuestUserId = selectedUserInfo?.id
        val thirdExpectedValue =
            setUpUsers(
                count = 2,
                hasGuest = true,
                selectedIndex = 1,
            )
        underTest.refreshUsers()
        assertThat(userInfos).isEqualTo(thirdExpectedValue)
        assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
        assertThat(selectedUserInfo?.isGuest).isTrue()
        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
    }

    private fun setUpUsers(
        count: Int,
        hasGuest: Boolean = false,
        selectedIndex: Int = 0,
    ): List<UserInfo> {
        val userInfos =
            (0 until count).map { index ->
                createUserInfo(
                    index,
                    isGuest = hasGuest && index == count - 1,
                )
            }
        whenever(manager.aliveUsers).thenReturn(userInfos)
        tracker.set(userInfos, selectedIndex)
        return userInfos
    }

    private fun createUserInfo(
        id: Int,
        isGuest: Boolean,
    ): UserInfo {
        val flags = 0
        return UserInfo(
            id,
            "user_$id",
            /* iconPath= */ "",
            flags,
            if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
        )
    }

    private fun setUpGlobalSettings(
        isSimpleUserSwitcher: Boolean = false,
        isAddUsersFromLockscreen: Boolean = false,
        isUserSwitcherEnabled: Boolean = true,
    ) {
        context.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
            true,
        )
        globalSettings.putIntForUser(
            UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
            if (isSimpleUserSwitcher) 1 else 0,
            UserHandle.USER_SYSTEM,
        )
        globalSettings.putIntForUser(
            Settings.Global.ADD_USERS_WHEN_LOCKED,
            if (isAddUsersFromLockscreen) 1 else 0,
            UserHandle.USER_SYSTEM,
        )
        globalSettings.putIntForUser(
            Settings.Global.USER_SWITCHER_ENABLED,
            if (isUserSwitcherEnabled) 1 else 0,
            UserHandle.USER_SYSTEM,
        )
    }

    private fun assertUserSwitcherSettings(
        model: UserSwitcherSettingsModel?,
        expectedSimpleUserSwitcher: Boolean,
        expectedAddUsersFromLockscreen: Boolean,
        expectedUserSwitcherEnabled: Boolean,
    ) {
        checkNotNull(model)
        assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
        assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
        assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
    }

    /**
     * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
     * is then automatically canceled and cleaned-up.
     */
    private fun runSelfCancelingTest(
        block: suspend CoroutineScope.() -> Unit,
    ) =
        runBlocking(Dispatchers.Main.immediate) {
            val scope = CoroutineScope(coroutineContext + Job())
            block(scope)
            scope.cancel()
        }
}
Loading