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