diff options
author | 2023-11-14 14:03:27 -0500 | |
---|---|---|
committer | 2023-11-15 10:01:26 -0500 | |
commit | 43fe454f23a95ec11f25620975340bf392ddb161 (patch) | |
tree | f646ca7c6334ddab9b2edef99c67a9ae349e8b6c | |
parent | 55da290431232e15b5a8c175561ea57796b572d5 (diff) |
Rename UserDataSource to UserRepository
Adds requestState to allow modification of user profile state,
including availability (quiet mode).
Test: UserRepositoryImplTest
Change-Id: Ic38f24475c73390841ee599c48d965117981faa0
-rw-r--r-- | java/src/com/android/intentresolver/v2/data/User.kt | 75 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/v2/data/model/User.kt | 50 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt | 29 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt (renamed from java/src/com/android/intentresolver/v2/data/UserDataSource.kt) | 50 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt (renamed from java/src/com/android/intentresolver/v2/data/UserDataSourceModule.kt) | 6 | ||||
-rw-r--r-- | java/tests/src/com/android/intentresolver/v2/data/UserDataSourceImplTest.kt | 194 | ||||
-rw-r--r-- | java/tests/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt | 222 | ||||
-rw-r--r-- | java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt | 39 |
8 files changed, 382 insertions, 283 deletions
diff --git a/java/src/com/android/intentresolver/v2/data/User.kt b/java/src/com/android/intentresolver/v2/data/User.kt deleted file mode 100644 index d8a4af74..00000000 --- a/java/src/com/android/intentresolver/v2/data/User.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.android.intentresolver.v2.data - -import android.annotation.UserIdInt -import android.content.pm.UserInfo -import android.os.UserHandle -import com.android.intentresolver.v2.data.User.Role -import com.android.intentresolver.v2.data.User.Type -import com.android.intentresolver.v2.data.User.Type.FULL -import com.android.intentresolver.v2.data.User.Type.PROFILE - -/** - * A User represents the owner of a distinct set of content. - * * maps 1:1 to a UserHandle or UserId (Int) value. - * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the - * [type] property. - * - * See - * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md) - * - * ``` - * fun example() { - * User(id = 0, role = PERSONAL) - * User(id = 10, role = WORK) - * User(id = 11, role = CLONE) - * User(id = 12, role = PRIVATE) - * } - * ``` - */ -data class User( - @UserIdInt val id: Int, - val role: Role, -) { - val handle: UserHandle = UserHandle.of(id) - - val type: Type - get() = role.type - - enum class Type { - FULL, - PROFILE - } - - enum class Role( - /** The type of the role user. */ - val type: Type - ) { - PERSONAL(FULL), - PRIVATE(PROFILE), - WORK(PROFILE), - CLONE(PROFILE) - } -} - -fun UserInfo.getSupportedUserRole(): Role? = - when { - isFull -> Role.PERSONAL - isManagedProfile -> Role.WORK - isCloneProfile -> Role.CLONE - isPrivateProfile -> Role.PRIVATE - else -> null - } - -/** - * Creates a [User], based on values from a [UserInfo]. - * - * ``` - * val users: List<User> = - * getEnabledProfiles(user).map(::toUser).filterNotNull() - * ``` - * - * @return a [User] if the [UserInfo] matched a supported [Role], otherwise null - */ -fun UserInfo.toUser(): User? { - return getSupportedUserRole()?.let { role -> User(userHandle.identifier, role) } -} diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/data/model/User.kt new file mode 100644 index 00000000..504b04c8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/model/User.kt @@ -0,0 +1,50 @@ +package com.android.intentresolver.v2.data.model + +import android.annotation.UserIdInt +import android.os.UserHandle +import com.android.intentresolver.v2.data.model.User.Type +import com.android.intentresolver.v2.data.model.User.Type.FULL +import com.android.intentresolver.v2.data.model.User.Type.PROFILE + +/** + * A User represents the owner of a distinct set of content. + * * maps 1:1 to a UserHandle or UserId (Int) value. + * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the + * [type] property. + * + * See + * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md) + * + * ``` + * val users = listOf( + * User(id = 0, role = PERSONAL), + * User(id = 10, role = WORK), + * User(id = 11, role = CLONE), + * User(id = 12, role = PRIVATE), + * ) + * ``` + */ +data class User( + @UserIdInt val id: Int, + val role: Role, +) { + val handle: UserHandle = UserHandle.of(id) + + val type: Type + get() = role.type + + enum class Type { + FULL, + PROFILE + } + + enum class Role( + /** The type of the role user. */ + val type: Type + ) { + PERSONAL(FULL), + PRIVATE(PROFILE), + WORK(PROFILE), + CLONE(PROFILE) + } +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt new file mode 100644 index 00000000..fc82efee --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt @@ -0,0 +1,29 @@ +package com.android.intentresolver.v2.data.repository + +import android.content.pm.UserInfo +import com.android.intentresolver.v2.data.model.User +import com.android.intentresolver.v2.data.model.User.Role + +/** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */ +fun UserInfo.getSupportedUserRole(): Role? = + when { + isFull -> Role.PERSONAL + isManagedProfile -> Role.WORK + isCloneProfile -> Role.CLONE + isPrivateProfile -> Role.PRIVATE + else -> null + } + +/** + * Creates a [User], based on values from a [UserInfo]. + * + * ``` + * val users: List<User> = + * getEnabledProfiles(user).map(::toUser).filterNotNull() + * ``` + * + * @return a [User] if the [UserInfo] matched a supported [Role], otherwise null + */ +fun UserInfo.toUser(): User? { + return getSupportedUserRole()?.let { role -> User(userHandle.identifier, role) } +} diff --git a/java/src/com/android/intentresolver/v2/data/UserDataSource.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt index 9eecc3be..dc809b46 100644 --- a/java/src/com/android/intentresolver/v2/data/UserDataSource.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt @@ -1,4 +1,4 @@ -package com.android.intentresolver.v2.data +package com.android.intentresolver.v2.data.repository import android.content.Context import android.content.Intent @@ -19,7 +19,9 @@ import androidx.annotation.VisibleForTesting import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.Main import com.android.intentresolver.inject.ProfileParent -import com.android.intentresolver.v2.data.UserDataSourceImpl.UserEvent +import com.android.intentresolver.v2.data.broadcastFlow +import com.android.intentresolver.v2.data.model.User +import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -35,7 +37,7 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext -interface UserDataSource { +interface UserRepository { /** * A [Flow] user profile groups. Each map contains the context user along with all members of * the profile group. This includes the (Full) parent user, if the context user is a profile. @@ -47,17 +49,31 @@ interface UserDataSource { * * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled]. */ - fun isAvailable(handle: UserHandle): Flow<Boolean> + fun isAvailable(user: User): Flow<Boolean> + + /** + * Request that availability be updated to the requested state. This currently includes toggling + * quiet mode as needed. This may involve additional background actions, such as starting or + * stopping a profile user (along with their many associated processes). + * + * If successful, the change will be applied after the call returns and can be observed using + * [UserRepository.isAvailable] for the given user. + * + * No actions are taken if the user is already in requested state. + * + * @throws IllegalArgumentException if called for an unsupported user type + */ + suspend fun requestState(user: User, available: Boolean) } -private const val TAG = "UserDataSource" +private const val TAG = "UserRepository" private data class UserWithState(val user: User, val available: Boolean) private typealias UserStateMap = Map<UserHandle, UserWithState> /** Tracks and publishes state for the parent user and associated profiles. */ -class UserDataSourceImpl +class UserRepositoryImpl @VisibleForTesting constructor( private val profileParent: UserHandle, @@ -66,7 +82,7 @@ constructor( private val userEvents: Flow<UserEvent>, scope: CoroutineScope, private val backgroundDispatcher: CoroutineDispatcher -) : UserDataSource { +) : UserRepository { @Inject constructor( @ApplicationContext context: Context, @@ -130,10 +146,28 @@ constructor( private val availability: Flow<Map<UserHandle, Boolean>> = usersWithState.map { map -> map.mapValues { it.value.available } }.distinctUntilChanged() - override fun isAvailable(handle: UserHandle): Flow<Boolean> { + override fun isAvailable(user: User): Flow<Boolean> { + return isAvailable(user.handle) + } + + @VisibleForTesting + fun isAvailable(handle: UserHandle): Flow<Boolean> { return availability.map { it[handle] ?: false } } + override suspend fun requestState(user: User, available: Boolean) { + require(user.type == User.Type.PROFILE) { "Only profile users are supported" } + return requestState(user.handle, available) + } + + @VisibleForTesting + suspend fun requestState(user: UserHandle, available: Boolean) { + return withContext(backgroundDispatcher) { + Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user") + userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user) + } + } + private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap { val userEntry = current[event.user] diff --git a/java/src/com/android/intentresolver/v2/data/UserDataSourceModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt index 94f39eb7..94f985e7 100644 --- a/java/src/com/android/intentresolver/v2/data/UserDataSourceModule.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt @@ -1,4 +1,4 @@ -package com.android.intentresolver.v2.data +package com.android.intentresolver.v2.data.repository import android.content.Context import android.os.UserHandle @@ -15,7 +15,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface UserDataSourceModule { +interface UserRepositoryModule { companion object { @Provides @Singleton @@ -30,5 +30,5 @@ interface UserDataSourceModule { } } - @Binds @Singleton fun userDataSource(impl: UserDataSourceImpl): UserDataSource + @Binds @Singleton fun userRepository(impl: UserRepositoryImpl): UserRepository } diff --git a/java/tests/src/com/android/intentresolver/v2/data/UserDataSourceImplTest.kt b/java/tests/src/com/android/intentresolver/v2/data/UserDataSourceImplTest.kt deleted file mode 100644 index 56d5de35..00000000 --- a/java/tests/src/com/android/intentresolver/v2/data/UserDataSourceImplTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.intentresolver.v2.data - -import android.content.Intent.ACTION_PROFILE_ADDED -import android.content.Intent.ACTION_PROFILE_AVAILABLE -import android.content.Intent.ACTION_PROFILE_REMOVED -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserHandle.USER_NULL -import android.os.UserManager -import com.android.intentresolver.mock -import com.android.intentresolver.v2.coroutines.collectLastValue -import com.android.intentresolver.v2.data.User.Role -import com.android.intentresolver.v2.data.UserDataSourceImpl.UserEvent -import com.android.intentresolver.v2.platform.FakeUserManager -import com.android.intentresolver.v2.platform.FakeUserManager.ProfileType -import com.android.intentresolver.whenever -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.eq - -internal class UserDataSourceImplTest { - private val userManager = FakeUserManager() - private val userState = userManager.state - - @Test - fun initialization() = runTest { - val dataSource = createUserDataSource(userManager) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users) - .containsExactly( - userState.primaryUserHandle, - User(userState.primaryUserHandle.identifier, Role.PERSONAL) - ) - } - - @Test - fun createProfile() = runTest { - val dataSource = createUserDataSource(userManager) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users!!.values.filter { it.role.type == User.Type.PROFILE }).isEmpty() - - val profile = userState.createProfile(ProfileType.WORK) - assertThat(users).containsEntry(profile, User(profile.identifier, Role.WORK)) - } - - @Test - fun removeProfile() = runTest { - val dataSource = createUserDataSource(userManager) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - val work = userState.createProfile(ProfileType.WORK) - assertThat(users).containsEntry(work, User(work.identifier, Role.WORK)) - - userState.removeProfile(work) - assertThat(users).doesNotContainEntry(work, User(work.identifier, Role.WORK)) - } - - @Test - fun isAvailable() = runTest { - val dataSource = createUserDataSource(userManager) - val work = userState.createProfile(ProfileType.WORK) - - val available by collectLastValue(dataSource.isAvailable(work)) - assertThat(available).isTrue() - - userState.setQuietMode(work, true) - assertThat(available).isFalse() - - userState.setQuietMode(work, false) - assertThat(available).isTrue() - } - - /** - * This and all the 'recovers_from_*' tests below all configure a static event flow instead of - * using [FakeUserManager]. These tests verify that a invalid broadcast causes the flow to - * reinitialize with the user profile group. - */ - @Test - fun recovers_from_invalid_profile_added_event() = runTest { - val userManager = - mockUserManager(validUser = UserHandle.USER_SYSTEM, invalidUser = USER_NULL) - val events = flowOf(UserEvent(ACTION_PROFILE_ADDED, UserHandle.of(USER_NULL))) - val dataSource = - UserDataSourceImpl( - profileParent = UserHandle.SYSTEM, - userManager = userManager, - userEvents = events, - scope = backgroundScope, - backgroundDispatcher = Dispatchers.Unconfined - ) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users) - .containsExactly(UserHandle.SYSTEM, User(UserHandle.USER_SYSTEM, Role.PERSONAL)) - } - - @Test - fun recovers_from_invalid_profile_removed_event() = runTest { - val userManager = - mockUserManager(validUser = UserHandle.USER_SYSTEM, invalidUser = USER_NULL) - val events = flowOf(UserEvent(ACTION_PROFILE_REMOVED, UserHandle.of(USER_NULL))) - val dataSource = - UserDataSourceImpl( - profileParent = UserHandle.SYSTEM, - userManager = userManager, - userEvents = events, - scope = backgroundScope, - backgroundDispatcher = Dispatchers.Unconfined - ) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users) - .containsExactly(UserHandle.SYSTEM, User(UserHandle.USER_SYSTEM, Role.PERSONAL)) - } - - @Test - fun recovers_from_invalid_profile_available_event() = runTest { - val userManager = - mockUserManager(validUser = UserHandle.USER_SYSTEM, invalidUser = USER_NULL) - val events = flowOf(UserEvent(ACTION_PROFILE_AVAILABLE, UserHandle.of(USER_NULL))) - val dataSource = - UserDataSourceImpl( - UserHandle.SYSTEM, - userManager, - events, - backgroundScope, - Dispatchers.Unconfined - ) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users) - .containsExactly(UserHandle.SYSTEM, User(UserHandle.USER_SYSTEM, Role.PERSONAL)) - } - - @Test - fun recovers_from_unknown_event() = runTest { - val userManager = - mockUserManager(validUser = UserHandle.USER_SYSTEM, invalidUser = USER_NULL) - val events = flowOf(UserEvent("UNKNOWN_EVENT", UserHandle.of(USER_NULL))) - val dataSource = - UserDataSourceImpl( - profileParent = UserHandle.SYSTEM, - userManager = userManager, - userEvents = events, - scope = backgroundScope, - backgroundDispatcher = Dispatchers.Unconfined - ) - val users by collectLastValue(dataSource.users) - - assertWithMessage("collectLast(dataSource.users)").that(users).isNotNull() - assertThat(users) - .containsExactly(UserHandle.SYSTEM, User(UserHandle.USER_SYSTEM, Role.PERSONAL)) - } -} - -@Suppress("SameParameterValue", "DEPRECATION") -private fun mockUserManager(validUser: Int, invalidUser: Int) = - mock<UserManager> { - val info = UserInfo(validUser, "", "", UserInfo.FLAG_FULL) - doReturn(listOf(info)).whenever(this).getEnabledProfiles(anyInt()) - - doReturn(info).whenever(this).getUserInfo(eq(validUser)) - - doReturn(listOf<UserInfo>()).whenever(this).getEnabledProfiles(eq(invalidUser)) - - doReturn(null).whenever(this).getUserInfo(eq(invalidUser)) - } - -private fun TestScope.createUserDataSource(userManager: FakeUserManager) = - UserDataSourceImpl( - profileParent = userManager.state.primaryUserHandle, - userManager = userManager, - userEvents = userManager.state.userEvents, - scope = backgroundScope, - backgroundDispatcher = Dispatchers.Unconfined - ) diff --git a/java/tests/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/java/tests/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt new file mode 100644 index 00000000..4f514db5 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt @@ -0,0 +1,222 @@ +package com.android.intentresolver.v2.data.repository + +import android.content.Intent +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserHandle.SYSTEM +import android.os.UserHandle.USER_SYSTEM +import android.os.UserManager +import com.android.intentresolver.mock +import com.android.intentresolver.v2.coroutines.collectLastValue +import com.android.intentresolver.v2.data.model.User +import com.android.intentresolver.v2.data.model.User.Role +import com.android.intentresolver.v2.platform.FakeUserManager +import com.android.intentresolver.v2.platform.FakeUserManager.ProfileType +import com.android.intentresolver.whenever +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.doReturn + +internal class UserRepositoryImplTest { + private val userManager = FakeUserManager() + private val userState = userManager.state + + @Test + fun initialization() = runTest { + val repo = createUserRepository(userManager) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users) + .containsExactly( + userState.primaryUserHandle, + User(userState.primaryUserHandle.identifier, Role.PERSONAL) + ) + } + + @Test + fun createProfile() = runTest { + val repo = createUserRepository(userManager) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users!!.values.filter { it.role.type == User.Type.PROFILE }).isEmpty() + + val profile = userState.createProfile(ProfileType.WORK) + assertThat(users).containsEntry(profile, User(profile.identifier, Role.WORK)) + } + + @Test + fun removeProfile() = runTest { + val repo = createUserRepository(userManager) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + val work = userState.createProfile(ProfileType.WORK) + assertThat(users).containsEntry(work, User(work.identifier, Role.WORK)) + + userState.removeProfile(work) + assertThat(users).doesNotContainEntry(work, User(work.identifier, Role.WORK)) + } + + @Test + fun isAvailable() = runTest { + val repo = createUserRepository(userManager) + val work = userState.createProfile(ProfileType.WORK) + + val available by collectLastValue(repo.isAvailable(work)) + assertThat(available).isTrue() + + userState.setQuietMode(work, true) + assertThat(available).isFalse() + + userState.setQuietMode(work, false) + assertThat(available).isTrue() + } + + @Test + fun requestState() = runTest { + val repo = createUserRepository(userManager) + val work = userState.createProfile(ProfileType.WORK) + + val available by collectLastValue(repo.isAvailable(work)) + assertThat(available).isTrue() + + repo.requestState(work, false) + assertThat(available).isFalse() + + repo.requestState(work, true) + assertThat(available).isTrue() + } + + @Test(expected = IllegalArgumentException::class) + fun requestState_invalidForFullUser() = runTest { + val repo = createUserRepository(userManager) + val primaryUser = User(userState.primaryUserHandle.identifier, Role.PERSONAL) + repo.requestState(primaryUser, available = false) + } + + /** + * This and all the 'recovers_from_*' tests below all configure a static event flow instead of + * using [FakeUserManager]. These tests verify that a invalid broadcast causes the flow to + * reinitialize with the user profile group. + */ + @Test + fun recovers_from_invalid_profile_added_event() = runTest { + val userManager = + mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL) + val events = + flowOf( + UserRepositoryImpl.UserEvent( + Intent.ACTION_PROFILE_ADDED, + UserHandle.of(UserHandle.USER_NULL) + ) + ) + val repo = + UserRepositoryImpl( + profileParent = SYSTEM, + userManager = userManager, + userEvents = events, + scope = backgroundScope, + backgroundDispatcher = Dispatchers.Unconfined + ) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL)) + } + + @Test + fun recovers_from_invalid_profile_removed_event() = runTest { + val userManager = + mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL) + val events = + flowOf( + UserRepositoryImpl.UserEvent( + Intent.ACTION_PROFILE_REMOVED, + UserHandle.of(UserHandle.USER_NULL) + ) + ) + val repo = + UserRepositoryImpl( + profileParent = SYSTEM, + userManager = userManager, + userEvents = events, + scope = backgroundScope, + backgroundDispatcher = Dispatchers.Unconfined + ) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL)) + } + + @Test + fun recovers_from_invalid_profile_available_event() = runTest { + val userManager = + mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL) + val events = + flowOf( + UserRepositoryImpl.UserEvent( + Intent.ACTION_PROFILE_AVAILABLE, + UserHandle.of(UserHandle.USER_NULL) + ) + ) + val repo = + UserRepositoryImpl(SYSTEM, userManager, events, backgroundScope, Dispatchers.Unconfined) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL)) + } + + @Test + fun recovers_from_unknown_event() = runTest { + val userManager = + mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL) + val events = + flowOf( + UserRepositoryImpl.UserEvent("UNKNOWN_EVENT", UserHandle.of(UserHandle.USER_NULL)) + ) + val repo = + UserRepositoryImpl( + profileParent = SYSTEM, + userManager = userManager, + userEvents = events, + scope = backgroundScope, + backgroundDispatcher = Dispatchers.Unconfined + ) + val users by collectLastValue(repo.users) + + assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull() + assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL)) + } +} + +@Suppress("SameParameterValue", "DEPRECATION") +private fun mockUserManager(validUser: Int, invalidUser: Int) = + mock<UserManager> { + val info = UserInfo(validUser, "", "", UserInfo.FLAG_FULL) + doReturn(listOf(info)).whenever(this).getEnabledProfiles(Mockito.anyInt()) + + doReturn(info).whenever(this).getUserInfo(Mockito.eq(validUser)) + + doReturn(listOf<UserInfo>()).whenever(this).getEnabledProfiles(Mockito.eq(invalidUser)) + + doReturn(null).whenever(this).getUserInfo(Mockito.eq(invalidUser)) + } + +private fun TestScope.createUserRepository(userManager: FakeUserManager) = + UserRepositoryImpl( + profileParent = userManager.state.primaryUserHandle, + userManager = userManager, + userEvents = userManager.state.userEvents, + scope = backgroundScope, + backgroundDispatcher = Dispatchers.Unconfined + ) diff --git a/java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt b/java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt index ef1e5917..370e5a00 100644 --- a/java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt +++ b/java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt @@ -1,7 +1,10 @@ package com.android.intentresolver.v2.platform import android.content.Context +import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE +import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE import android.content.Intent.ACTION_PROFILE_ADDED +import android.content.Intent.ACTION_PROFILE_AVAILABLE import android.content.Intent.ACTION_PROFILE_REMOVED import android.content.Intent.ACTION_PROFILE_UNAVAILABLE import android.content.pm.UserInfo @@ -12,9 +15,10 @@ import android.content.pm.UserInfo.NO_PROFILE_GROUP_ID import android.os.IUserManager import android.os.UserHandle import android.os.UserManager +import androidx.annotation.NonNull import com.android.intentresolver.THROWS_EXCEPTION import com.android.intentresolver.mock -import com.android.intentresolver.v2.data.UserDataSourceImpl.UserEvent +import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent import com.android.intentresolver.v2.platform.FakeUserManager.State import com.android.intentresolver.whenever import kotlin.random.Random @@ -77,6 +81,14 @@ class FakeUserManager(val state: State = State()) : } } + override fun requestQuietModeEnabled( + enableQuietMode: Boolean, + @NonNull userHandle: UserHandle + ): Boolean { + state.setQuietMode(userHandle, enableQuietMode) + return true + } + override fun isQuietModeEnabled(userHandle: UserHandle): Boolean { return state.getUser(userHandle).isQuietModeEnabled } @@ -136,8 +148,29 @@ class FakeUserManager(val state: State = State()) : } fun setQuietMode(user: UserHandle, quietMode: Boolean) { - userInfoMap[user]?.also { it.flags = it.flags or UserInfo.FLAG_QUIET_MODE } - eventChannel.trySend(UserEvent(ACTION_PROFILE_UNAVAILABLE, user, quietMode)) + userInfoMap[user]?.also { + it.flags = + if (quietMode) { + it.flags or UserInfo.FLAG_QUIET_MODE + } else { + it.flags and UserInfo.FLAG_QUIET_MODE.inv() + } + val actions = mutableListOf<String>() + if (quietMode) { + actions += ACTION_PROFILE_UNAVAILABLE + if (it.isManagedProfile) { + actions += ACTION_MANAGED_PROFILE_UNAVAILABLE + } + } else { + actions += ACTION_PROFILE_AVAILABLE + if (it.isManagedProfile) { + actions += ACTION_MANAGED_PROFILE_AVAILABLE + } + } + actions.forEach { action -> + eventChannel.trySend(UserEvent(action, user, quietMode)) + } + } } fun createProfile(type: ProfileType, parent: UserHandle = primaryUserHandle): UserHandle { |