blob: 9eecc3bee21ad267dd7bfc8bcd0f14c2467598be [file] [log] [blame]
package com.android.intentresolver.v2.data
import android.content.Context
import android.content.Intent
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.Intent.EXTRA_QUIET_MODE
import android.content.Intent.EXTRA_USER
import android.content.IntentFilter
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
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 dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
interface UserDataSource {
/**
* 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.
*/
val users: Flow<Map<UserHandle, User>>
/**
* A [Flow] of availability. Only profile users may become unavailable.
*
* Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].
*/
fun isAvailable(handle: UserHandle): Flow<Boolean>
}
private const val TAG = "UserDataSource"
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
@VisibleForTesting
constructor(
private val profileParent: UserHandle,
private val userManager: UserManager,
/** A flow of events which represent user-state changes from [UserManager]. */
private val userEvents: Flow<UserEvent>,
scope: CoroutineScope,
private val backgroundDispatcher: CoroutineDispatcher
) : UserDataSource {
@Inject
constructor(
@ApplicationContext context: Context,
@ProfileParent profileParent: UserHandle,
userManager: UserManager,
@Main scope: CoroutineScope,
@Background background: CoroutineDispatcher
) : this(
profileParent,
userManager,
userEvents = userBroadcastFlow(context, profileParent),
scope,
background
)
data class UserEvent(val action: String, val user: UserHandle, val quietMode: Boolean = false)
/**
* An exception which indicates that an inconsistency exists between the user state map and the
* rest of the system.
*/
internal class UserStateException(
override val message: String,
val event: UserEvent,
override val cause: Throwable? = null
) : RuntimeException("$message: event=$event", cause)
private val usersWithState: Flow<UserStateMap> =
userEvents
.onStart { emit(UserEvent(INITIALIZE, profileParent)) }
.onEach { Log.i("UserDataSource", "userEvent: $it") }
.runningFold<UserEvent, UserStateMap>(emptyMap()) { users, event ->
try {
// Handle an action by performing some operation, then returning a new map
when (event.action) {
INITIALIZE -> createNewUserStateMap(profileParent)
ACTION_PROFILE_ADDED -> handleProfileAdded(event, users)
ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users)
ACTION_MANAGED_PROFILE_UNAVAILABLE,
ACTION_MANAGED_PROFILE_AVAILABLE,
ACTION_PROFILE_AVAILABLE,
ACTION_PROFILE_UNAVAILABLE -> handleAvailability(event, users)
else -> {
Log.w(TAG, "Unhandled event: $event)")
users
}
}
} catch (e: UserStateException) {
Log.e(TAG, "An error occurred handling an event: ${e.event}", e)
Log.e(TAG, "Attempting to recover...")
createNewUserStateMap(profileParent)
}
}
.onEach { Log.i("UserDataSource", "userStateMap: $it") }
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
.filterNot { it.isEmpty() }
override val users: Flow<Map<UserHandle, User>> =
usersWithState.map { map -> map.mapValues { it.value.user } }.distinctUntilChanged()
private val availability: Flow<Map<UserHandle, Boolean>> =
usersWithState.map { map -> map.mapValues { it.value.available } }.distinctUntilChanged()
override fun isAvailable(handle: UserHandle): Flow<Boolean> {
return availability.map { it[handle] ?: false }
}
private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap {
val userEntry =
current[event.user]
?: throw UserStateException("User was not present in the map", event)
return current + (event.user to userEntry.copy(available = !event.quietMode))
}
private fun handleProfileRemoved(event: UserEvent, current: UserStateMap): UserStateMap {
if (!current.containsKey(event.user)) {
throw UserStateException("User was not present in the map", event)
}
return current.filterKeys { it != event.user }
}
private suspend fun handleProfileAdded(event: UserEvent, current: UserStateMap): UserStateMap {
val user =
try {
requireNotNull(readUser(event.user))
} catch (e: Exception) {
throw UserStateException("Failed to read user from UserManager", event, e)
}
return current + (event.user to UserWithState(user, !event.quietMode))
}
private suspend fun createNewUserStateMap(user: UserHandle): UserStateMap {
val profiles = readProfileGroup(user)
return profiles
.mapNotNull { userInfo ->
userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) }
}
.associateBy { it.user.handle }
}
private suspend fun readProfileGroup(handle: UserHandle): List<UserInfo> {
return withContext(backgroundDispatcher) {
@Suppress("DEPRECATION") userManager.getEnabledProfiles(handle.identifier)
}
.toList()
}
/** Read [UserInfo] from [UserManager], or null if not found or an unsupported type. */
private suspend fun readUser(user: UserHandle): User? {
val userInfo =
withContext(backgroundDispatcher) { userManager.getUserInfo(user.identifier) }
return userInfo?.let { info ->
info.getSupportedUserRole()?.let { role -> User(info.id, role) }
}
}
}
/** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */
private fun Intent.toUserEvent(): UserEvent? {
val action = action
val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java)
val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) ?: false
return if (user == null || action == null) {
null
} else {
UserEvent(action, user, quietMode)
}
}
const val INITIALIZE = "INITIALIZE"
private fun createFilter(actions: Iterable<String>): IntentFilter {
return IntentFilter().apply { actions.forEach(::addAction) }
}
private fun UserInfo?.isAvailable(): Boolean {
return this?.isQuietModeEnabled != true
}
private fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> {
val userActions =
setOf(
ACTION_PROFILE_ADDED,
ACTION_PROFILE_REMOVED,
// Quiet mode enabled/disabled for managed
// From: UserController.broadcastProfileAvailabilityChanges
// In response to setQuietModeEnabled
ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only
ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only
// Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile
// true'
ACTION_PROFILE_AVAILABLE, // quiet mode,
ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type
)
return broadcastFlow(context, createFilter(userActions), profileParent, Intent::toUserEvent)
}