summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author mrenouf <mrenouf@google.com> 2023-11-14 14:03:27 -0500
committer Mark Renouf <mrenouf@google.com> 2023-11-15 10:01:26 -0500
commit43fe454f23a95ec11f25620975340bf392ddb161 (patch)
treef646ca7c6334ddab9b2edef99c67a9ae349e8b6c
parent55da290431232e15b5a8c175561ea57796b572d5 (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.kt75
-rw-r--r--java/src/com/android/intentresolver/v2/data/model/User.kt50
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt29
-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.kt194
-rw-r--r--java/tests/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt222
-rw-r--r--java/tests/src/com/android/intentresolver/v2/platform/FakeUserManager.kt39
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 {