blob: ef1e5917a6074bea5694fa7b264cb672f9fed9ba [file] [log] [blame]
package com.android.intentresolver.v2.platform
import android.content.Context
import android.content.Intent.ACTION_PROFILE_ADDED
import android.content.Intent.ACTION_PROFILE_REMOVED
import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
import android.content.pm.UserInfo
import android.content.pm.UserInfo.FLAG_FULL
import android.content.pm.UserInfo.FLAG_INITIALIZED
import android.content.pm.UserInfo.FLAG_PROFILE
import android.content.pm.UserInfo.NO_PROFILE_GROUP_ID
import android.os.IUserManager
import android.os.UserHandle
import android.os.UserManager
import com.android.intentresolver.THROWS_EXCEPTION
import com.android.intentresolver.mock
import com.android.intentresolver.v2.data.UserDataSourceImpl.UserEvent
import com.android.intentresolver.v2.platform.FakeUserManager.State
import com.android.intentresolver.whenever
import kotlin.random.Random
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import org.mockito.Mockito.RETURNS_SELF
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.withSettings
/**
* A stand-in for [UserManager] to support testing of data layer components which depend on it.
*
* This fake targets system applications which need to interact with any or all of the current
* user's associated profiles (as reported by [getEnabledProfiles]). Support for manipulating
* non-profile (full) secondary users (switching active foreground user, adding or removing users)
* is not included.
*
* Upon creation [FakeUserManager] contains a single primary (full) user with a randomized ID. This
* is available from [FakeUserManager.state] using [primaryUserHandle][State.primaryUserHandle] or
* [getPrimaryUser][State.getPrimaryUser].
*
* To make state changes, use functions available from [FakeUserManager.state]:
* * [createProfile][State.createProfile]
* * [removeProfile][State.removeProfile]
* * [setQuietMode][State.setQuietMode]
*
* Any functionality not explicitly overridden here is guaranteed to throw an exception when
* accessed (access to the real system service is prevented).
*/
class FakeUserManager(val state: State = State()) :
UserManager(/* context = */ mockContext(), /* service = */ mockService()) {
enum class ProfileType {
WORK,
CLONE,
PRIVATE
}
override fun getProfileParent(userHandle: UserHandle): UserHandle? {
return state.getUserOrNull(userHandle)?.let { user ->
if (user.isProfile) {
state.getUserOrNull(UserHandle.of(user.profileGroupId))?.userHandle
} else {
null
}
}
}
override fun getUserInfo(userId: Int): UserInfo? {
return state.getUserOrNull(UserHandle.of(userId))
}
@Suppress("OVERRIDE_DEPRECATION")
override fun getEnabledProfiles(userId: Int): List<UserInfo> {
val user = state.users.single { it.id == userId }
return state.users.filter { other ->
user.id == other.id || user.profileGroupId == other.profileGroupId
}
}
override fun isQuietModeEnabled(userHandle: UserHandle): Boolean {
return state.getUser(userHandle).isQuietModeEnabled
}
override fun toString(): String {
return "FakeUserManager(state=$state)"
}
class State {
private val eventChannel = Channel<UserEvent>()
private val userInfoMap: MutableMap<UserHandle, UserInfo> = mutableMapOf()
/** The id of the primary/full/system user, which is automatically created. */
val primaryUserHandle: UserHandle
/**
* Retrieves the primary user. The value returned changes, but the values are immutable.
*
* Do not cache this value in tests, between operations.
*/
fun getPrimaryUser(): UserInfo = getUser(primaryUserHandle)
private var nextUserId: Int = 100 + Random.nextInt(0, 900)
/**
* A flow of [UserEvent] which emulates those normally generated from system broadcasts.
*
* Events are produced by calls to [createPrimaryUser], [createProfile], [removeProfile].
*/
val userEvents: Flow<UserEvent>
val users: List<UserInfo>
get() = userInfoMap.values.toList()
val userHandles: List<UserHandle>
get() = userInfoMap.keys.toList()
init {
primaryUserHandle = createPrimaryUser(allocateNextId())
userEvents = eventChannel.consumeAsFlow()
}
private fun allocateNextId() = nextUserId++
private fun createPrimaryUser(id: Int): UserHandle {
val userInfo =
UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_FULL, USER_TYPE_FULL_SYSTEM)
userInfoMap[userInfo.userHandle] = userInfo
return userInfo.userHandle
}
fun getUserOrNull(handle: UserHandle): UserInfo? = userInfoMap[handle]
fun getUser(handle: UserHandle): UserInfo =
requireNotNull(getUserOrNull(handle)) {
"Expected userInfoMap to contain an entry for $handle"
}
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))
}
fun createProfile(type: ProfileType, parent: UserHandle = primaryUserHandle): UserHandle {
val parentUser = getUser(parent)
require(!parentUser.isProfile) { "Parent user cannot be a profile" }
// Ensure the parent user has a valid profileGroupId
if (parentUser.profileGroupId == NO_PROFILE_GROUP_ID) {
parentUser.profileGroupId = parentUser.id
}
val id = allocateNextId()
val userInfo =
UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_PROFILE, type.toUserType()).apply {
profileGroupId = parentUser.profileGroupId
}
userInfoMap[userInfo.userHandle] = userInfo
eventChannel.trySend(UserEvent(ACTION_PROFILE_ADDED, userInfo.userHandle))
return userInfo.userHandle
}
fun removeProfile(handle: UserHandle): Boolean {
return userInfoMap[handle]?.let { user ->
require(user.isProfile) { "Only profiles can be removed" }
userInfoMap.remove(user.userHandle)
eventChannel.trySend(UserEvent(ACTION_PROFILE_REMOVED, user.userHandle))
return true
}
?: false
}
override fun toString() = buildString {
append("State(nextUserId=$nextUserId, userInfoMap=[")
userInfoMap.entries.forEach {
append("UserHandle[${it.key.identifier}] = ${it.value.debugString},")
}
append("])")
}
}
}
/** A safe mock of [Context] which throws on any unstubbed method call. */
private fun mockContext(user: UserHandle = UserHandle.SYSTEM): Context {
return mock<Context>(withSettings().defaultAnswer(THROWS_EXCEPTION)) {
doAnswer(RETURNS_SELF).whenever(this).applicationContext
doReturn(user).whenever(this).user
doReturn(user.identifier).whenever(this).userId
}
}
private fun FakeUserManager.ProfileType.toUserType(): String {
return when (this) {
FakeUserManager.ProfileType.WORK -> UserManager.USER_TYPE_PROFILE_MANAGED
FakeUserManager.ProfileType.CLONE -> UserManager.USER_TYPE_PROFILE_CLONE
FakeUserManager.ProfileType.PRIVATE -> UserManager.USER_TYPE_PROFILE_PRIVATE
}
}
/** A safe mock of [IUserManager] which throws on any unstubbed method call. */
fun mockService(): IUserManager {
return mock<IUserManager>(withSettings().defaultAnswer(THROWS_EXCEPTION))
}
val UserInfo.debugString: String
get() =
"UserInfo(id=$id, profileGroupId=$profileGroupId, name=$name, " +
"type=$userType, flags=${UserInfo.flagsToString(flags)})"