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
This commit is contained in:
Alejandro Nijamkin
2022-09-27 17:59:26 -07:00
parent 59ad257ef3
commit 32b5486aa6
9 changed files with 817 additions and 199 deletions

View File

@@ -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,
)

View File

@@ -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<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. */
val isActionableWhenLocked: Flow<Boolean>
@@ -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<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 {
fun send() {
trySendWithFailureLogging(
@@ -99,11 +174,148 @@ constructor(
override val actions: Flow<List<UserActionModel>> =
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 = 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"
}
}

View File

@@ -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,
)

View File

@@ -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<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) }
}
}
}

View File

@@ -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<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()
}
}

View File

@@ -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<UserSwitcherController.UserSwitchCallback>
@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<UserModel>? = 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<UserActionModel>? = 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
}
}

View File

@@ -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<UserSwitcherController.UserSwitchCallback>
@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<UserModel>? = 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<UserActionModel>? = 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,
)
}
}

View File

@@ -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<UserInfo> = emptyList(),
private var _userId: Int = 0,
private var _userHandle: UserHandle = UserHandle.of(_userId),
private var _userInfo: UserInfo = mock(),
private var _userProfiles: List<UserInfo> = emptyList(),
userContentResolver: ContentResolver = MockContentResolver(),
userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker {
val callbacks = mutableListOf<UserTracker.Callback>()
override val userId: Int = userId
override val userHandle: UserHandle = userHandle
override val userInfo: UserInfo = userInfo
override val userProfiles: List<UserInfo> = userProfiles
override val userId: Int
get() = _userId
override val userHandle: UserHandle
get() = _userHandle
override val userInfo: UserInfo
get() = _userInfo
override val userProfiles: List<UserInfo>
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<UserInfo>, selectedUserIndex: Int) {
_userProfiles = userInfos
_userInfo = userInfos[selectedUserIndex]
_userId = _userInfo.id
_userHandle = UserHandle.of(_userId)
callbacks.forEach { it.onUserChanged(_userId, userContext) }
}
}

View File

@@ -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<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
_userSwitcherSettings.asStateFlow()
private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _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<UserInfo>) {
_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<UserModel>) {
_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<UserActionModel>) {
@@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
fun setGuestUserResetting(value: Boolean) {
_isGuestUserResetting = value
}
}