diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt new file mode 100644 index 0000000000000..4fd55c0e21c81 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt @@ -0,0 +1,25 @@ +/* + * 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, +) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 035638800f9cf..b85e85d01b692 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,9 +18,13 @@ package com.android.systemui.user.data.repository import android.content.Context +import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import com.android.internal.util.UserIcons 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.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.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.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord 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.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose 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.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Acts as source of truth for user related data. @@ -55,6 +80,18 @@ interface UserRepository { /** List of available user-related actions. */ val actions: Flow> + /** User switcher related settings. */ + val userSwitcherSettings: Flow + + /** List of all users on the device. */ + val userInfos: Flow> + + /** [UserInfo] of the currently-selected user. */ + val selectedUserInfo: Flow + + /** User ID of the last non-guest selected user. */ + val lastSelectedNonGuestUserId: Int + /** Whether actions are available even when locked. */ val isActionableWhenLocked: Flow @@ -62,7 +99,23 @@ interface UserRepository { val isGuestUserAutoCreated: Boolean /** 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 @@ -71,9 +124,31 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, 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 { + private val isNewImpl: Boolean + get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + + private val _userSwitcherSettings = MutableStateFlow(null) + override val userSwitcherSettings: Flow = + _userSwitcherSettings.asStateFlow().filterNotNull() + + private val _userInfos = MutableStateFlow?>(null) + override val userInfos: Flow> = _userInfos.filterNotNull() + + private val _selectedUserInfo = MutableStateFlow(null) + override val selectedUserInfo: Flow = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private set + private val userRecords: Flow> = conflatedCallbackFlow { fun send() { trySendWithFailureLogging( @@ -99,11 +174,148 @@ constructor( override val actions: Flow> = userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - override val isActionableWhenLocked: Flow = controller.isAddUsersFromLockScreenEnabled + override val isActionableWhenLocked: Flow = + if (isNewImpl) { + emptyFlow() + } else { + controller.isAddUsersFromLockScreenEnabled + } - override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated + override val isGuestUserAutoCreated: Boolean = + if (isNewImpl) { + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) + } else { + controller.isGuestUserAutoCreated + } - override val isGuestUserResetting: Boolean = controller.isGuestUserResetting + 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 } + + if (result != null) { + _userInfos.value = result + } + } + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + 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 { return when { @@ -125,6 +337,7 @@ constructor( image = getUserImage(this), isSelected = isCurrent, isSelectable = isSwitchToEnabled || isGuest, + isGuest = isGuest, ) } @@ -162,5 +375,6 @@ constructor( companion object { private const val TAG = "UserRepository" + @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index bf7977a600e99..2e9367126a4c0 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -32,4 +32,7 @@ data class UserModel( val isSelected: Boolean, /** Whether this use is selectable. A non-selectable user cannot be switched to. */ 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, ) diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt new file mode 100644 index 0000000000000..0b8257da8fb5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -0,0 +1,48 @@ +/* + * 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 { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + names.forEach { name -> registerContentObserverForUser(name, observer, userId) } + + awaitClose { unregisterContentObserver(observer) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt new file mode 100644 index 0000000000000..4a8e0552d7785 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -0,0 +1,204 @@ +/* + * 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? = 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 { + 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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6fec343d036cc..6568f5f680e77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,201 +17,54 @@ package com.android.systemui.user.data.repository -import android.content.pm.UserInfo import android.os.UserManager -import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -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.ArgumentCaptor -import org.mockito.Captor +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplTest : SysuiTestCase() { +abstract class UserRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var manager: UserManager - @Mock private lateinit var controller: UserSwitcherController - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var controller: UserSwitcherController - private lateinit var underTest: UserRepositoryImpl + protected lateinit var underTest: UserRepositoryImpl - @Before - fun setUp() { + protected lateinit var globalSettings: FakeSettings + protected lateinit var tracker: FakeUserTracker + protected lateinit var featureFlags: FakeFeatureFlags + + protected fun setUp(isRefactored: Boolean) { MockitoAnnotations.initMocks(this) - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - underTest = - UserRepositoryImpl( - appContext = context, - manager = manager, - controller = controller, - ) + globalSettings = FakeSettings() + tracker = FakeUserTracker() + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored) } - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) - - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actopms - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - job.cancel() - } - - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) - } - - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, + protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + return UserRepositoryImpl( + appContext = context, + manager = manager, + controller = controller, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + globalSettings = globalSettings, + tracker = tracker, + featureFlags = featureFlags, ) } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt new file mode 100644 index 0000000000000..d4b41c18e123e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -0,0 +1,205 @@ +/* + * 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 androidx.test.filters.SmallTest +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +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.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Captor + private lateinit var userSwitchCallbackCaptor: + ArgumentCaptor + + @Before + fun setUp() { + super.setUp(isRefactored = false) + + whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) + whenever(controller.isGuestUserAutoCreated).thenReturn(false) + whenever(controller.isGuestUserResetting).thenReturn(false) + + underTest = create() + } + + @Test + fun `users - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `users - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `users - does not include actions`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List? = null + val job = underTest.users.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)?.id).isEqualTo(0) + assertThat(models?.get(0)?.isSelected).isTrue() + assertThat(models?.get(1)?.id).isEqualTo(1) + assertThat(models?.get(1)?.isSelected).isFalse() + assertThat(models?.get(2)?.id).isEqualTo(2) + assertThat(models?.get(2)?.isSelected).isFalse() + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createUserRecord(1), + createUserRecord(2), + ) + ) + var id: Int? = null + val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + + assertThat(id).isEqualTo(0) + + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0), + createUserRecord(1), + createUserRecord(2, isSelected = true), + ) + ) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + userSwitchCallbackCaptor.value.onUserSwitched() + assertThat(id).isEqualTo(2) + + job.cancel() + } + + @Test + fun `actions - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `actions - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `actions - does not include users`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List? = null + val job = underTest.actions.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) + assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) + assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + job.cancel() + } + + private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { + return UserRecord( + info = UserInfo(id, "name$id", 0), + isCurrent = isSelected, + ) + } + + private fun createActionRecord(action: UserActionModel): UserRecord { + return UserRecord( + isAddUser = action == UserActionModel.ADD_USER, + isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, + isGuest = action == UserActionModel.ENTER_GUEST_MODE, + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index b2b176420e402..9726bf83b263e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -26,20 +26,24 @@ import java.util.concurrent.Executor /** A fake [UserTracker] to be used in tests. */ class FakeUserTracker( - userId: Int = 0, - userHandle: UserHandle = UserHandle.of(userId), - userInfo: UserInfo = mock(), - userProfiles: List = emptyList(), + private var _userId: Int = 0, + private var _userHandle: UserHandle = UserHandle.of(_userId), + private var _userInfo: UserInfo = mock(), + private var _userProfiles: List = emptyList(), userContentResolver: ContentResolver = MockContentResolver(), userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { val callbacks = mutableListOf() - override val userId: Int = userId - override val userHandle: UserHandle = userHandle - override val userInfo: UserInfo = userInfo - override val userProfiles: List = userProfiles + override val userId: Int + get() = _userId + override val userHandle: UserHandle + get() = _userHandle + override val userInfo: UserInfo + get() = _userInfo + override val userProfiles: List + get() = _userProfiles override val userContentResolver: ContentResolver = userContentResolver override val userContext: Context = userContext @@ -55,4 +59,13 @@ class FakeUserTracker( override fun createCurrentUserContext(context: Context): Context { return onCreateCurrentUserContext(context) } + + fun set(userInfos: List, selectedUserIndex: Int) { + _userProfiles = userInfos + _userInfo = userInfos[selectedUserIndex] + _userId = _userInfo.id + _userHandle = UserHandle.of(_userId) + + callbacks.forEach { it.onUserChanged(_userId, userContext) } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 20f1e367944fb..4df8aa42ea2f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -17,12 +17,18 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { @@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository { private val _actions = MutableStateFlow>(emptyList()) override val actions: Flow> = _actions.asStateFlow() + private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) + override val userSwitcherSettings: Flow = + _userSwitcherSettings.asStateFlow() + + private val _userInfos = MutableStateFlow>(emptyList()) + override val userInfos: Flow> = _userInfos.asStateFlow() + + private val _selectedUserInfo = MutableStateFlow(null) + override val selectedUserInfo: Flow = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private val _isActionableWhenLocked = MutableStateFlow(false) override val isActionableWhenLocked: Flow = _isActionableWhenLocked.asStateFlow() private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated - private var _isGuestUserResetting: Boolean = false - override val isGuestUserResetting: Boolean - get() = _isGuestUserResetting + + override var isGuestUserResetting: Boolean = false + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL + + override var isRefreshUsersPaused: Boolean = false + + var refreshUsersCallCount: Int = 0 + private set + + override fun refreshUsers() { + refreshUsersCallCount++ + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return _userSwitcherSettings.value.isSimpleUserSwitcher + } + + fun setUserInfos(infos: List) { + _userInfos.value = infos + } + + suspend fun setSelectedUserInfo(userInfo: UserInfo) { + check(_userInfos.value.contains(userInfo)) { + "Cannot select the following user, it is not in the list of user infos: $userInfo!" + } + + _selectedUserInfo.value = userInfo + yield() + } + + suspend fun setSettings(settings: UserSwitcherSettingsModel) { + _userSwitcherSettings.value = settings + yield() + } fun setUsers(models: List) { _users.value = models } - fun setSelectedUser(userId: Int) { + suspend fun setSelectedUser(userId: Int) { check(_users.value.find { it.id == userId } != null) { "Cannot select a user with ID $userId - no user with that ID found!" } @@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository { } } ) + yield() } fun setActions(models: List) { @@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } - - fun setGuestUserResetting(value: Boolean) { - _isGuestUserResetting = value - } }