summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Mark Renouf <mrenouf@google.com> 2024-01-23 12:21:43 -0500
committer Mark Renouf <mrenouf@google.com> 2024-01-25 19:56:19 -0500
commitf30cb97a784ba508a82863ef74ea0135355aad0c (patch)
treefee5089bc3b3bf2b33e5caa9851303e7b0c52b07
parentc5d725eec15d2cff2aab08437948d6d0f2d01a63 (diff)
UserInteractor and Profile
A domain component which applies business logic to User data. This component maps Users to Profiles, the set of which is defined by Profile.Type Bug: 309960444 Test: atest IntentResolver-tests-unit:UserInteractorTest \ FakeUserRepositoryTest Change-Id: I9832836ae019ba1b0ae45366f9fc0e26bc9b23ce
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt4
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt24
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt7
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt2
-rw-r--r--java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt92
-rw-r--r--java/src/com/android/intentresolver/v2/domain/model/Profile.kt53
-rw-r--r--java/src/com/android/intentresolver/v2/shared/model/User.kt (renamed from java/src/com/android/intentresolver/v2/data/model/User.kt)23
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt61
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt108
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt26
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt179
11 files changed, 539 insertions, 40 deletions
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
index fc82efee..a0b2d1ef 100644
--- a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
@@ -1,8 +1,8 @@
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
+import com.android.intentresolver.v2.shared.model.User
+import com.android.intentresolver.v2.shared.model.User.Role
/** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */
fun UserInfo.getSupportedUserRole(): Role? =
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
index d2011aed..91ad6409 100644
--- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
@@ -20,8 +20,8 @@ import com.android.intentresolver.inject.Background
import com.android.intentresolver.inject.Main
import com.android.intentresolver.inject.ProfileParent
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 com.android.intentresolver.v2.shared.model.User
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -49,7 +49,7 @@ interface UserRepository {
*
* Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].
*/
- fun isAvailable(user: User): Flow<Boolean>
+ val availability: Flow<Map<User, Boolean>>
/**
* Request that availability be updated to the requested state. This currently includes toggling
@@ -145,30 +145,16 @@ constructor(
override val users: Flow<List<User>> =
usersWithState.map { userStateMap -> userStateMap.map { it.user } }.distinctUntilChanged()
- private val availability: Flow<Map<UserHandle, Boolean>> =
+ override val availability: Flow<Map<User, Boolean>> =
usersWithState
- .map { list -> list.associateBy { it.user.handle }.mapValues { it.value.available } }
+ .map { list -> list.associate { it.user to it.available } }
.distinctUntilChanged()
- 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)
+ userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user.handle)
}
}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
index 94f985e7..a84342f4 100644
--- a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
@@ -25,8 +25,11 @@ interface UserRepositoryModule {
@Provides
@Singleton
@ProfileParent
- fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle {
- return userManager.getProfileParent(user) ?: user
+ fun profileParent(
+ @ApplicationContext context: Context,
+ userManager: UserManager
+ ): UserHandle {
+ return userManager.getProfileParent(context.user) ?: context.user
}
}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
index 7ee78d91..3553744a 100644
--- a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
@@ -2,7 +2,7 @@ package com.android.intentresolver.v2.data.repository
import android.content.Context
import androidx.core.content.getSystemService
-import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.shared.model.User
/**
* Provides cached instances of a [system service][Context.getSystemService] created with
diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt
new file mode 100644
index 00000000..e1b3fb36
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.domain.interactor
+
+import android.os.UserHandle
+import com.android.intentresolver.inject.ApplicationUser
+import com.android.intentresolver.v2.data.repository.UserRepository
+import com.android.intentresolver.v2.domain.model.Profile
+import com.android.intentresolver.v2.domain.model.Profile.Type
+import com.android.intentresolver.v2.shared.model.User
+import com.android.intentresolver.v2.shared.model.User.Role
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+
+/** The high level User interface. */
+class UserInteractor
+@Inject
+constructor(
+ private val userRepository: UserRepository,
+ /** The specific [User] of the application which started this one. */
+ @ApplicationUser val launchedAs: UserHandle,
+) {
+ /** The profile group associated with the launching app user. */
+ val profiles: Flow<List<Profile>> =
+ userRepository.users.map { users ->
+ users.mapNotNull { user ->
+ when (user.role) {
+ // PERSONAL includes CLONE
+ Role.PERSONAL -> {
+ Profile(Type.PERSONAL, user, users.firstOrNull { it.role == Role.CLONE })
+ }
+ Role.CLONE -> {
+ /* ignore, included above */
+ null
+ }
+ // others map 1:1
+ else -> Profile(profileFromRole(user.role), user)
+ }
+ }
+ }
+
+ /** The [Profile] of the application which started this one. */
+ val launchedAsProfile: Flow<Profile> =
+ profiles.map { profiles ->
+ // The launching user profile is the one with a primary id or clone id
+ // matching the application user id. By definition there must always be exactly
+ // one matching profile for the current user.
+ profiles.single {
+ it.primary.id == launchedAs.identifier || it.clone?.id == launchedAs.identifier
+ }
+ }
+
+ /**
+ * Provides a flow to report on the availability of the profile. An unavailable profile may be
+ * hidden or appear disabled within the app.
+ */
+ fun isAvailable(type: Type): Flow<Boolean> {
+ val profileFlow = profiles.map { list -> list.firstOrNull { it.type == type } }
+ return combine(profileFlow, userRepository.availability) { profile, availability ->
+ when (profile) {
+ null -> false
+ else -> availability.getOrDefault(profile.primary, false)
+ }
+ }
+ }
+
+ private fun profileFromRole(role: Role): Type =
+ when (role) {
+ Role.PERSONAL -> Type.PERSONAL
+ Role.CLONE -> Type.PERSONAL /* CLONE maps to PERSONAL */
+ Role.PRIVATE -> Type.PRIVATE
+ Role.WORK -> Type.WORK
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/domain/model/Profile.kt b/java/src/com/android/intentresolver/v2/domain/model/Profile.kt
new file mode 100644
index 00000000..46015c7a
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/domain/model/Profile.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.domain.model
+
+import com.android.intentresolver.v2.domain.model.Profile.Type
+import com.android.intentresolver.v2.shared.model.User
+
+/**
+ * A domain layer model which associates [users][User] into a [Type] instance.
+ *
+ * This is a simple abstraction which combines a primary [user][User] with an optional
+ * [cloned apps][User.Role.CLONE] user. This encapsulates the cloned app user id, while still being
+ * available where needed.
+ */
+data class Profile(
+ val type: Type,
+ val primary: User,
+ /**
+ * An optional [User] of which contains second instances of some applications installed for the
+ * personal user. This value may only be supplied when creating the PERSONAL profile.
+ */
+ val clone: User? = null
+) {
+
+ init {
+ clone?.apply {
+ require(primary.role == User.Role.PERSONAL) {
+ "clone is not supported for profile=${this@Profile.type} / primary=$primary"
+ }
+ require(role == User.Role.CLONE) { "clone is not a clone user ($this)" }
+ }
+ }
+
+ enum class Type {
+ PERSONAL,
+ WORK,
+ PRIVATE
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/shared/model/User.kt
index 504b04c8..97db3280 100644
--- a/java/src/com/android/intentresolver/v2/data/model/User.kt
+++ b/java/src/com/android/intentresolver/v2/shared/model/User.kt
@@ -1,10 +1,25 @@
-package com.android.intentresolver.v2.data.model
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.shared.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
+import com.android.intentresolver.v2.shared.model.User.Type.FULL
+import com.android.intentresolver.v2.shared.model.User.Type.PROFILE
/**
* A User represents the owner of a distinct set of content.
diff --git a/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt
new file mode 100644
index 00000000..5ed6f506
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.data.repository
+
+import com.android.intentresolver.v2.shared.model.User
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+
+/** A simple repository which can be initialized from a list and updated. */
+class FakeUserRepository(vararg userList: User) : UserRepository {
+ internal data class UserState(val user: User, val available: Boolean)
+
+ private val userState = MutableStateFlow(userList.map { UserState(it, available = true) })
+
+ // Expose a List<User> from List<UserState>
+ override val users = userState.map { userList -> userList.map { it.user } }
+
+ fun addUser(user: User, available: Boolean) {
+ require(userState.value.none { it.user.id == user.id }) {
+ "A User with ${user.id} already exists!"
+ }
+ userState.update { it + UserState(user, available) }
+ }
+
+ fun removeUser(user: User) {
+ require(userState.value.any { it.user.id == user.id }) {
+ "A User with ${user.id} does not exist!"
+ }
+ userState.update { it.filterNot { state -> state.user.id == user.id } }
+ }
+
+ override val availability =
+ userState.map { userStateList -> userStateList.associate { it.user to it.available } }
+
+ override suspend fun requestState(user: User, available: Boolean) {
+ userState.update { userStateList ->
+ userStateList.map { userState ->
+ if (userState.user.id == user.id) {
+ UserState(user, available)
+ } else {
+ userState
+ }
+ }
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt
new file mode 100644
index 00000000..334f31ad
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.data.repository
+
+import com.android.intentresolver.v2.coroutines.collectLastValue
+import com.android.intentresolver.v2.shared.model.User
+import com.google.common.truth.Truth.assertThat
+import kotlin.random.Random
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class FakeUserRepositoryTest {
+ private val baseId = Random.nextInt(1000, 2000)
+
+ private val personalUser = User(id = baseId, role = User.Role.PERSONAL)
+ private val cloneUser = User(id = baseId + 1, role = User.Role.CLONE)
+ private val workUser = User(id = baseId + 2, role = User.Role.WORK)
+ private val privateUser = User(id = baseId + 3, role = User.Role.PRIVATE)
+
+ @Test
+ fun init() = runTest {
+ val repo = FakeUserRepository(personalUser, workUser, privateUser)
+
+ val users by collectLastValue(repo.users)
+ assertThat(users).containsExactly(personalUser, workUser, privateUser)
+ }
+
+ @Test
+ fun addUser() = runTest {
+ val repo = FakeUserRepository()
+
+ val users by collectLastValue(repo.users)
+ assertThat(users).isEmpty()
+
+ repo.addUser(personalUser, true)
+ assertThat(users).containsExactly(personalUser)
+
+ repo.addUser(workUser, false)
+ assertThat(users).containsExactly(personalUser, workUser)
+ }
+
+ @Test
+ fun removeUser() = runTest {
+ val repo = FakeUserRepository(personalUser, workUser)
+
+ val users by collectLastValue(repo.users)
+ repo.removeUser(workUser)
+ assertThat(users).containsExactly(personalUser)
+
+ repo.removeUser(personalUser)
+ assertThat(users).isEmpty()
+ }
+
+ @Test
+ fun isAvailable_defaultValue() = runTest {
+ val repo = FakeUserRepository(personalUser, workUser)
+
+ val available by collectLastValue(repo.availability)
+
+ repo.requestState(workUser, false)
+ assertThat(available!![workUser]).isFalse()
+
+ repo.requestState(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+
+ @Test
+ fun isAvailable() = runTest {
+ val repo = FakeUserRepository(personalUser, workUser)
+
+ val available by collectLastValue(repo.availability)
+ assertThat(available!![workUser]).isTrue()
+
+ repo.requestState(workUser, false)
+ assertThat(available!![workUser]).isFalse()
+
+ repo.requestState(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+
+ @Test
+ fun isAvailable_addRemove() = runTest {
+ val repo = FakeUserRepository(personalUser, workUser)
+
+ val available by collectLastValue(repo.availability)
+ assertThat(available!![workUser]).isTrue()
+
+ repo.removeUser(workUser)
+ assertThat(available!![workUser]).isNull()
+
+ repo.addUser(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
index 77f47285..6c61dfd6 100644
--- a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
@@ -8,10 +8,10 @@ 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.v2.shared.model.User
+import com.android.intentresolver.v2.shared.model.User.Role
import com.android.intentresolver.whenever
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -66,30 +66,32 @@ internal class UserRepositoryImplTest {
fun isAvailable() = runTest {
val repo = createUserRepository(userManager)
val work = userState.createProfile(ProfileType.WORK)
+ val workUser = User(work.identifier, Role.WORK)
- val available by collectLastValue(repo.isAvailable(work))
- assertThat(available).isTrue()
+ val available by collectLastValue(repo.availability)
+ assertThat(available?.get(workUser)).isTrue()
userState.setQuietMode(work, true)
- assertThat(available).isFalse()
+ assertThat(available?.get(workUser)).isFalse()
userState.setQuietMode(work, false)
- assertThat(available).isTrue()
+ assertThat(available?.get(workUser)).isTrue()
}
@Test
fun requestState() = runTest {
val repo = createUserRepository(userManager)
val work = userState.createProfile(ProfileType.WORK)
+ val workUser = User(work.identifier, Role.WORK)
- val available by collectLastValue(repo.isAvailable(work))
- assertThat(available).isTrue()
+ val available by collectLastValue(repo.availability)
+ assertThat(available?.get(workUser)).isTrue()
- repo.requestState(work, false)
- assertThat(available).isFalse()
+ repo.requestState(workUser, false)
+ assertThat(available?.get(workUser)).isFalse()
- repo.requestState(work, true)
- assertThat(available).isTrue()
+ repo.requestState(workUser, true)
+ assertThat(available?.get(workUser)).isTrue()
}
@Test(expected = IllegalArgumentException::class)
diff --git a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt
new file mode 100644
index 00000000..6fa055ef
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 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.intentresolver.v2.domain.interactor
+
+import com.android.intentresolver.v2.coroutines.collectLastValue
+import com.android.intentresolver.v2.data.repository.FakeUserRepository
+import com.android.intentresolver.v2.domain.model.Profile
+import com.android.intentresolver.v2.domain.model.Profile.Type.PERSONAL
+import com.android.intentresolver.v2.domain.model.Profile.Type.PRIVATE
+import com.android.intentresolver.v2.domain.model.Profile.Type.WORK
+import com.android.intentresolver.v2.shared.model.User
+import com.android.intentresolver.v2.shared.model.User.Role
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.random.Random
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class UserInteractorTest {
+ private val baseId = Random.nextInt(1000, 2000)
+
+ private val personalUser = User(id = baseId, role = Role.PERSONAL)
+ private val cloneUser = User(id = baseId + 1, role = Role.CLONE)
+ private val workUser = User(id = baseId + 2, role = Role.WORK)
+ private val privateUser = User(id = baseId + 3, role = Role.PRIVATE)
+
+ @Test
+ fun launchedByProfile(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(personalUser, cloneUser),
+ launchedAs = personalUser.handle
+ )
+
+ val launchedAsProfile by collectLastValue(profileInteractor.launchedAsProfile)
+
+ assertThat(launchedAsProfile).isEqualTo(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun launchedByProfile_asClone(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(personalUser, cloneUser),
+ launchedAs = cloneUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.launchedAsProfile)
+
+ assertThat(profiles).isEqualTo(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withPersonal(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(personalUser),
+ launchedAs = personalUser.handle
+ )
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser))
+ }
+
+ @Test
+ fun profiles_addClone(): Unit = runTest {
+ val fakeUserRepo = FakeUserRepository(personalUser)
+ val profileInteractor =
+ UserInteractor(userRepository = fakeUserRepo, launchedAs = personalUser.handle)
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser))
+
+ fakeUserRepo.addUser(cloneUser, available = true)
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withPersonalAndClone(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(personalUser, cloneUser),
+ launchedAs = personalUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withAllSupportedTypes(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(personalUser, cloneUser, workUser, privateUser),
+ launchedAs = personalUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles)
+ .containsExactly(
+ Profile(PERSONAL, personalUser, cloneUser),
+ Profile(WORK, workUser),
+ Profile(PRIVATE, privateUser)
+ )
+ }
+
+ @Test
+ fun profiles_preservesIterationOrder(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(workUser, cloneUser, privateUser, personalUser),
+ launchedAs = personalUser.handle
+ )
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles)
+ .containsExactly(
+ Profile(WORK, workUser),
+ Profile(PRIVATE, privateUser),
+ Profile(PERSONAL, personalUser, cloneUser),
+ )
+ }
+
+ @Test
+ fun isAvailable_defaultValue() = runTest {
+ val userRepo = FakeUserRepository(personalUser)
+ userRepo.addUser(workUser, false)
+
+ val profileInteractor =
+ UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle)
+ val personalAvailable by collectLastValue(profileInteractor.isAvailable(PERSONAL))
+ val workAvailable by collectLastValue(profileInteractor.isAvailable(WORK))
+
+ assertWithMessage("personalAvailable").that(personalAvailable!!).isTrue()
+
+ assertWithMessage("workAvailable").that(workAvailable!!).isFalse()
+ }
+
+ @Test
+ fun isAvailable() = runTest {
+ val userRepo = FakeUserRepository(workUser, personalUser)
+ val profileInteractor =
+ UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle)
+ val workAvailable by collectLastValue(profileInteractor.isAvailable(WORK))
+
+ // Default state is enabled in FakeUserManager
+ assertWithMessage("workAvailable").that(workAvailable).isTrue()
+
+ // Making user unavailable makes profile unavailable
+ userRepo.requestState(workUser, false)
+ assertWithMessage("workAvailable").that(workAvailable).isFalse()
+
+ // Making user available makes profile available again
+ userRepo.requestState(workUser, true)
+ assertWithMessage("workAvailable").that(workAvailable).isTrue()
+
+ // When a user is removed availability should update to false
+ userRepo.removeUser(workUser)
+ assertWithMessage("workAvailable").that(workAvailable).isFalse()
+ }
+}