1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.photopicker.core.user 18 19 import android.content.BroadcastReceiver 20 import android.content.ContentResolver 21 import android.content.Context 22 import android.content.Intent 23 import android.content.IntentFilter 24 import android.content.pm.PackageManager 25 import android.content.pm.ResolveInfo 26 import android.content.pm.UserProperties 27 import android.content.res.Resources 28 import android.os.UserHandle 29 import android.os.UserManager 30 import android.util.Log 31 import androidx.compose.ui.graphics.asImageBitmap 32 import androidx.core.graphics.drawable.toBitmap 33 import com.android.modules.utils.build.SdkLevel 34 import com.android.photopicker.core.configuration.PhotopickerConfiguration 35 import com.android.photopicker.extensions.requireSystemService 36 import kotlinx.coroutines.CoroutineDispatcher 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.channels.awaitClose 39 import kotlinx.coroutines.flow.MutableStateFlow 40 import kotlinx.coroutines.flow.SharingStarted 41 import kotlinx.coroutines.flow.StateFlow 42 import kotlinx.coroutines.flow.callbackFlow 43 import kotlinx.coroutines.flow.stateIn 44 import kotlinx.coroutines.flow.update 45 import kotlinx.coroutines.launch 46 47 /** 48 * Provides a long-living [StateFlow] that represents the current application's [UserStatus]. This 49 * class also provides methods to switch the current active profile. 50 * 51 * This is provided as a part of Core and will be lazily initialized to prevent it from being 52 * created before it is needed, but it will live as a singleton for the life of the activity once it 53 * has been initialized. 54 * 55 * Will emit a value immediately of the current list available [UserProfile] as well as the current 56 * Active profile. (Initialized as the Profile of the user that owns the process the [Activity] is 57 * running in.) 58 * 59 * Additionally, this class registers a [BroadcastReceiver] on behalf of the activity to subscribe 60 * to profile changes as they happen on the device, although those are subject to delivery delays 61 * depending on how busy the device currently is (and if Photopicker is currently in the 62 * foreground). 63 * 64 * @param context The context of the Application this UserMonitor is provided in. 65 * @param configuration a [PhotopickerConfiguration] flow from the [ConfigurationManager] 66 * @property scope The [CoroutineScope] that the BroadcastReceiver will listen in. 67 * @property dispatcher [CoroutineDispatcher] scope that the BroadcastReceiver will listen in. 68 * @property processOwnerUserHandle the user handle of the process that owns the Photopicker 69 * session. 70 */ 71 class UserMonitor( 72 context: Context, 73 private val configuration: StateFlow<PhotopickerConfiguration>, 74 private val scope: CoroutineScope, 75 private val dispatcher: CoroutineDispatcher, 76 private val processOwnerUserHandle: UserHandle, 77 ) { 78 79 companion object { 80 const val TAG: String = "PhotopickerUserMonitor" 81 } 82 83 private val userManager: UserManager = context.requireSystemService() 84 private val packageManager: PackageManager = context.packageManager 85 86 /** 87 * Internal state flow that the external flow is derived from. When making state changes, this 88 * is the flow that should be updated. 89 */ 90 private val _userStatus: MutableStateFlow<UserStatus> = 91 MutableStateFlow( 92 UserStatus( 93 activeUserProfile = getUserProfileFromHandle(processOwnerUserHandle, context), 94 allProfiles = 95 userManager.userProfiles 96 .filter { 97 // Filter out any profiles that should not be shown in sharing surfaces. 98 if (SdkLevel.isAtLeastV()) { 99 val properties = userManager.getUserProperties(it) 100 properties.getShowInSharingSurfaces() == 101 UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE 102 } else { 103 true 104 } 105 } 106 .map { getUserProfileFromHandle(it, context) }, 107 activeContentResolver = getContentResolver(context, processOwnerUserHandle) 108 ) 109 ) 110 111 /** 112 * This flow exposes the current internal [UserStatus], and replays the most recent value for 113 * new subscribers. 114 */ 115 val userStatus: StateFlow<UserStatus> = 116 _userStatus.stateIn( 117 scope, 118 SharingStarted.WhileSubscribed(), 119 initialValue = _userStatus.value 120 ) 121 122 /** Setup a BroadcastReceiver to receive broadcasts for profile availability changes */ 123 private val profileChanges = 124 callbackFlow<Pair<Intent, Context>> { 125 val receiver = 126 object : BroadcastReceiver() { 127 override fun onReceive(context: Context, intent: Intent) { 128 Log.d(TAG, "Received profile changed: $intent") 129 trySend(Pair(intent, context)) 130 } 131 } 132 val intentFilter = IntentFilter() 133 134 // It's ok if these intents send duplicate broadcasts, the resulting state is only 135 // updated & emitted if something actually changed. (Meaning duplicate broadcasts will 136 // not cause subscribers to be notified, although there is a marginal cost to parse the 137 // profile state again) 138 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) 139 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) 140 141 if (SdkLevel.isAtLeastS()) { 142 // On S+ devices use the broader profile listners to capture other types of 143 // profiles. 144 intentFilter.addAction(Intent.ACTION_PROFILE_ACCESSIBLE) 145 intentFilter.addAction(Intent.ACTION_PROFILE_INACCESSIBLE) 146 } 147 148 /* 149 TODO(b/303779617) 150 This broadcast receiver should be launched in the parent profile of the user since 151 child profiles do not receive these broadcasts. 152 */ 153 if (SdkLevel.isAtLeastT()) { 154 context.registerReceiver(receiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) 155 } else { 156 // manually set the flags since [Context.RECEIVER_NOT_EXPORTED] doesn't exist pre 157 // Sdk33 158 context.registerReceiver(receiver, intentFilter, /* flags=*/ 0x4) 159 } 160 161 awaitClose { 162 Log.d(TAG, """BroadcastReceiver was closed, unregistering.""") 163 context.unregisterReceiver(receiver) 164 } 165 } 166 167 init { 168 // Begin to collect emissions from the BroadcastReceiver. Started in the init block 169 // to ensure only one collection is ever started. This collection is launched in the 170 // injected scope with the injected dispatcher. 171 scope.launch(dispatcher) { 172 profileChanges.collect { (intent, context) -> 173 handleProfileChangeBroadcast(intent, context) 174 } 175 } 176 } 177 178 /** 179 * Attempt to switch the Active [UserProfile] to a known profile that matches the passed 180 * [UserProfile]. 181 * 182 * This is not guaranteed to succeed. The target profile type may be disabled, not exist or 183 * already be active. If the profile switch is successful, [UserMonitor] will emit a new 184 * [UserStatus] with the updated state. 185 * 186 * @return The [SwitchProfileResult] of the requested change. 187 */ 188 suspend fun requestSwitchActiveUserProfile( 189 requested: UserProfile, 190 context: Context 191 ): SwitchUserProfileResult { 192 193 // Attempt to find the requested profile amongst the profiles known. 194 val profile: UserProfile? = 195 _userStatus.value.allProfiles.find { it.identifier == requested.identifier } 196 197 profile?.let { 198 199 // Only allow the switch if a profile is currently enabled. 200 if (profile.enabled) { 201 _userStatus.update { 202 it.copy( 203 activeUserProfile = profile, 204 activeContentResolver = 205 getContentResolver(context, UserHandle.of(profile.identifier)) 206 ) 207 } 208 return SwitchUserProfileResult.SUCCESS 209 } 210 211 return SwitchUserProfileResult.FAILED_PROFILE_DISABLED 212 } 213 214 return SwitchUserProfileResult.FAILED_UNKNOWN_PROFILE 215 } 216 217 /** 218 * Handler for the incoming BroadcastReceiver emissions representing a profile state change. 219 * 220 * This handler will check the currently known profiles in the current user state and emit an 221 * updated user status value. 222 */ 223 private suspend fun handleProfileChangeBroadcast(intent: Intent, context: Context) { 224 225 val handle: UserHandle? = getUserHandleFromIntent(intent) 226 227 handle?.let { 228 Log.d( 229 TAG, 230 "Received a profile update for ${handle.getIdentifier()} from intent $intent" 231 ) 232 233 // Assemble a new UserProfile from the updated UserHandle. 234 val profile = getUserProfileFromHandle(handle, context) 235 236 // Generate a new list of profiles to in preparation for an update. 237 val newProfilesList: List<UserProfile> = 238 listOf( 239 // Copy the current list but remove the matching profile 240 *_userStatus.value.allProfiles 241 .filterNot { it.identifier == profile.identifier } 242 .toTypedArray(), 243 // Replace the matching profile with the updated one. 244 profile 245 ) 246 247 // Check and see if the profile we just updated is still enabled, and if it is the 248 // current active profile 249 if ( 250 !profile.enabled && 251 profile.identifier == _userStatus.value.activeUserProfile.identifier 252 ) { 253 Log.i( 254 TAG, 255 "The active profile is no longer enabled, transitioning back to the process" + 256 " owner's profile." 257 ) 258 259 // The current profile is disabled, we need to transition back to the process 260 // owner's profile. 261 val processOwnerProfile = 262 newProfilesList.find { it.identifier == processOwnerUserHandle.getIdentifier() } 263 264 processOwnerProfile?.let { 265 // Update userStatus with the updated list of UserProfiles. 266 _userStatus.update { 267 it.copy( 268 activeUserProfile = processOwnerProfile, 269 allProfiles = newProfilesList 270 ) 271 } 272 } 273 274 // This is potentially a problematic state, the current profile is disabled, 275 // and attempting to find the process owner's profile was unsuccessful. 276 ?: run { 277 Log.w( 278 TAG, 279 "Could not find the process owner's profile to switch to when the" + 280 " active profile was disabled." 281 ) 282 283 // Still attempt to update the list of profiles. 284 _userStatus.update { it.copy(allProfiles = newProfilesList) } 285 } 286 } else { 287 288 // Update userStatus with the updated list of UserProfiles. 289 _userStatus.update { it.copy(allProfiles = newProfilesList) } 290 } 291 } 292 // If the incoming Intent does not include a UserHandle, there is nothing to update, 293 // but Log a warning to help with debugging. 294 ?: run { 295 Log.w( 296 TAG, 297 "Received intent: $intent but could not find matching UserHandle. Ignoring." 298 ) 299 } 300 } 301 302 /** 303 * Determines if the current handle supports CrossProfile content sharing. 304 * 305 * @return Whether CrossProfile content sharing is supported in this handle. 306 */ 307 private fun getIsCrossProfileAllowedForHandle( 308 handle: UserHandle, 309 ): Boolean { 310 311 // First, check if cross profile is delegated to parent profile 312 if (SdkLevel.isAtLeastV()) { 313 val properties: UserProperties = userManager.getUserProperties(handle) 314 if ( 315 /* 316 * All user profiles with user property 317 * [UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT] 318 * can access each other including its parent. 319 */ 320 properties.getCrossProfileContentSharingStrategy() == 321 UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT 322 ) { 323 return true 324 } 325 } 326 327 // Next, inspect the current configuration and if there is an intent set, try to see 328 // if there is a matching CrossProfileIntentForwarder 329 configuration.value.intent?.let { 330 val intent = 331 it.clone() as? Intent // clone() returns an object so cast back to an Intent 332 intent?.let { 333 // Remove specific component / package info from the intent before querying 334 // package manager. (This is going to look for all handlers of this intent, 335 // and it shouldn't be scoped to a specific component or package) 336 it.setComponent(null) 337 it.setPackage(null) 338 339 for (info: ResolveInfo? in 340 packageManager.queryIntentActivities( 341 intent, 342 PackageManager.MATCH_DEFAULT_ONLY 343 )) { 344 info?.let { 345 if (it.isCrossProfileIntentForwarderActivity()) { 346 // This profile can handle cross profile content 347 // from the current context profile 348 return true 349 } 350 } 351 } 352 } 353 } 354 355 // Last resort, no applicable cross profile information found, so disallow cross-profile 356 // content to this profile. 357 return false 358 } 359 360 /** 361 * Assemble a [UserProfile] from a provided [UserHandle] 362 * 363 * @return A UserProfile that corresponds to the UserHandle. 364 */ 365 private fun getUserProfileFromHandle(handle: UserHandle, context: Context): UserProfile { 366 367 val isParentProfile = userManager.getProfileParent(handle) == null 368 val isManaged = userManager.isManagedProfile(handle.getIdentifier()) 369 val isQuietModeEnabled = userManager.isQuietModeEnabled(handle) 370 var isCrossProfileSupported = getIsCrossProfileAllowedForHandle(handle) 371 372 val userContext = context.createContextAsUser(handle, /* flags=*/ 0) 373 val localUserManager: UserManager = userContext.requireSystemService() 374 375 val (icon, label) = 376 with(localUserManager) { 377 if (SdkLevel.isAtLeastV()) { 378 try { 379 // Since these require an external call to generate, create them once 380 // and cache them in the profile that is getting passed to the UI to 381 // speed things up! 382 Pair(getUserBadge().toBitmap().asImageBitmap(), getProfileLabel()) 383 } catch (exception: Resources.NotFoundException) { 384 // If either resource is not defined by the system, fall back to the 385 // pre-compiled options to ensure that the UI doesn't end up in a weird 386 // state. 387 Pair(null, null) 388 } 389 } else { 390 // For Pre-V the UI will use pre-compiled resources and mappings to generate the 391 // icon. 392 Pair(null, null) 393 } 394 } 395 396 return UserProfile( 397 identifier = handle.getIdentifier(), 398 icon = icon, 399 label = label, 400 profileType = 401 when { 402 // Profiles that do not have a parent are considered the primary profile 403 isParentProfile -> UserProfile.ProfileType.PRIMARY 404 isManaged -> UserProfile.ProfileType.MANAGED 405 else -> UserProfile.ProfileType.UNKNOWN 406 }, 407 disabledReasons = 408 when (handle) { 409 // The profile is never disabled if it is the current process' profile 410 processOwnerUserHandle -> emptySet() 411 else -> 412 buildSet { 413 if (isParentProfile) 414 return@buildSet // Parent profile can always be accessed by children 415 if (isQuietModeEnabled) add(UserProfile.DisabledReason.QUIET_MODE) 416 if (!isCrossProfileSupported) 417 add(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED) 418 } 419 } 420 ) 421 } 422 423 /** 424 * Attempts to extract a user handle from the provided intent, using the [Intent.EXTRA_USER] 425 * key. 426 * 427 * @return the nullable UserHandle if the handle isn't provided, or if the object in 428 * [Intent.EXTRA_USER] isn't a [UserHandle] 429 */ 430 private suspend fun getUserHandleFromIntent(intent: Intent): UserHandle? { 431 432 if (SdkLevel.isAtLeastT()) 433 // Use the type-safe API when it's available. 434 return intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle::class.java) 435 else 436 @Suppress("DEPRECATION") 437 return intent.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle 438 } 439 440 /** @return the content resolver for given profile. */ 441 private fun getContentResolver(context: Context, userHandle: UserHandle): ContentResolver = 442 context 443 .createPackageContextAsUser(context.packageName, /* flags */ 0, userHandle) 444 .contentResolver 445 } 446