• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.configuration
18 
19 import android.content.Intent
20 import android.content.pm.PackageManager
21 import android.content.pm.ResolveInfo
22 import android.media.ApplicationMediaCapabilities
23 import android.net.Uri
24 import android.os.SystemProperties
25 import android.os.UserHandle
26 import android.provider.MediaStore
27 import android.util.Log
28 import com.android.photopicker.core.navigation.PhotopickerDestinations
29 
30 /** Check system properties to determine if the device is considered debuggable */
31 private val buildIsDebuggable = SystemProperties.getInt("ro.debuggable", 0) == 1
32 
33 /** The default selection maximum size if not set by the caller */
34 const val DEFAULT_SELECTION_LIMIT = 1
35 
36 /** Enum that describes the current runtime environment of the Photopicker. */
37 enum class PhotopickerRuntimeEnv {
38     ACTIVITY,
39     EMBEDDED,
40 }
41 
42 /**
43  * Data object that represents a possible configuration state of the Photopicker.
44  *
45  * @property runtimeEnv The current Photopicker runtime environment, this should never be changed
46  *   during configuration updates.
47  * @property action the [Intent#getAction] that Photopicker is currently serving.
48  * @property callingPackage the package name of the caller
49  * @property callingPackageUid the uid of the caller
50  * @property callingPackageLabel the display label of the caller that can be shown to the user
51  * @property callingPackageMediaCapabilities the value of [MediaStore.EXTRA_MEDIA_CAPABILITIES]. If
52  *   set, represents the [ApplicationMediaCapabilities] of the calling package.
53  * @property accentColor the accent color (if valid) from
54  *   [MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR]
55  * @property mimeTypes the mimetypes to filter all media requests with for the current session.
56  * @property pickImagesInOrder whether to show check marks as ordered number values for selected
57  *   media.
58  * @property selectionLimit the value of [MediaStore.EXTRA_PICK_IMAGES_MAX] with a default value of
59  *   [DEFAULT_SELECTION_LIMIT], and max value of [MediaStore.getPickImagesMaxLimit()] if it was not
60  *   set or set to too large a limit.
61  * @property startDestination the start destination that should be consider the "home" view the user
62  *   is shown for the session.
63  * @property preSelectedUris an [ArrayList] of the [Uri]s of the items selected by the user in the
64  *   previous photopicker sessions launched via the same calling app.
65  * @property flags a snapshot of the relevant flags in [DeviceConfig]. These are not live values.
66  * @property deviceIsDebuggable if the device is running a build which has [ro.debuggable == 1]
67  * @property intent the [Intent] that Photopicker was launched with. This property is private to
68  *   restrict access outside of this class.
69  * @property sessionId identifies the current photopicker session
70  */
71 data class PhotopickerConfiguration(
72     val runtimeEnv: PhotopickerRuntimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
73     val action: String,
74     val callingPackage: String? = null,
75     val callingPackageUid: Int? = null,
76     val callingPackageLabel: String? = null,
77     val callingPackageMediaCapabilities: ApplicationMediaCapabilities? = null,
78     val accentColor: Long? = null,
79     val mimeTypes: ArrayList<String> = arrayListOf("image/*", "video/*"),
80     val pickImagesInOrder: Boolean = false,
81     val selectionLimit: Int = DEFAULT_SELECTION_LIMIT,
82     val startDestination: PhotopickerDestinations = PhotopickerDestinations.DEFAULT,
83     val preSelectedUris: ArrayList<Uri>? = null,
84     val deviceIsDebuggable: Boolean = buildIsDebuggable,
85     val flags: PhotopickerFlags = PhotopickerFlags(),
86     val sessionId: Int,
87     private val intent: Intent? = null,
88 ) {
89 
90     /**
91      * Use the internal Intent to see if the Intent can be resolved as a
92      * CrossProfileIntentForwarderActivity for the target user.
93      *
94      * This method exists to limit the visibility of the intent field, but [UserMonitor] requires
95      * the intent to check for CrossProfileIntentForwarder's. Rather than exposing intent as a
96      * public field, this method can be called to do the check, if an Intent exists.
97      *
98      * @param packageManager the PM of the "from" user
99      * @param targetUserHandle the [UserHandle] of the target user
100      * @return Whether the current Intent Photopicker may be running under has a matching
101      *   CrossProfileIntentForwarderActivity
102      */
doesCrossProfileIntentForwarderExistsnull103     fun doesCrossProfileIntentForwarderExists(
104         packageManager: PackageManager,
105         fromUserHandle: UserHandle,
106         targetUserHandle: UserHandle,
107     ): Boolean {
108 
109         val intentToCheck: Intent? =
110             when (runtimeEnv) {
111                 PhotopickerRuntimeEnv.ACTIVITY ->
112                     // clone() returns an object so cast back to an Intent
113                     intent?.clone() as? Intent
114 
115                 // For the EMBEDDED runtime, no intent exists, so generate cross profile forwarding
116                 // based upon Photopicker's standard api ACTION_PICK_IMAGES
117                 PhotopickerRuntimeEnv.EMBEDDED -> Intent(MediaStore.ACTION_PICK_IMAGES)
118             }
119 
120         intentToCheck?.let {
121             // Remove specific component / package info from the intent before querying
122             // package manager. (This is going to look for all handlers of this intent,
123             // and it shouldn't be scoped to a specific component or package)
124             it.setComponent(null)
125             it.setPackage(null)
126 
127             for (info: ResolveInfo? in
128                 packageManager.queryIntentActivitiesAsUser(
129                     it,
130                     PackageManager.MATCH_DEFAULT_ONLY,
131                     fromUserHandle,
132                 )) {
133                 info?.let {
134                     if (it.isCrossProfileIntentForwarderActivity()) {
135 
136                         /*
137                          * IMPORTANT: This is a reflection based hack to ensure the profile is actually
138                          * the installer of the CrossProfileIntentForwardingActivity.
139                          *
140                          * ResolveInfo.targetUserId exists, but is a hidden API not available to
141                          * mainline modules, and no such API exists, so it is accessed via reflection
142                          * below. All exceptions are caught to protect against reflection related
143                          * issues such as:
144                          * NoSuchFieldException / IllegalAccessException / SecurityException.
145                          *
146                          * In the event of an exception, the code fails "closed" for the current
147                          * profile to avoid showing content that should not be visible.
148                          */
149                         val activityTargetUserId =
150                             try {
151                                 val property =
152                                     it::class.java.getDeclaredField("targetUserId").apply {
153                                         isAccessible = true
154                                     }
155                                 property.get(it) as? Int
156                             } catch (e: Exception) {
157                                 when (e) {
158                                     is NoSuchFieldException,
159                                     is IllegalAccessException,
160                                     is SecurityException -> {
161                                         Log.e(
162                                             ConfigurationManager.TAG,
163                                             "Could not reflect targetUserId field for cross " +
164                                                 "profile checks.",
165                                         )
166                                         // Any time we are unable to obtain the cross profile
167                                         // targetUserId, fail closed by returning false.
168                                         return@doesCrossProfileIntentForwarderExists false
169                                     }
170                                     else -> {
171                                         Log.e(
172                                             ConfigurationManager.TAG,
173                                             "Exception occurred during cross profile checks",
174                                             e,
175                                         )
176                                         null
177                                     }
178                                 }
179                             }
180                         if (activityTargetUserId == targetUserHandle.getIdentifier()) {
181                             Log.d(
182                                 ConfigurationManager.TAG,
183                                 "Found matching CrossProfileIntentForwarderActivity for " +
184                                     "targetUserId ${targetUserHandle.getIdentifier()}",
185                             )
186                             // This profile can handle cross profile content
187                             // from the current context profile
188                             return true
189                         }
190                     }
191                 }
192             }
193         }
194             // Log a warning that the intent was null, but probably shouldn't have been.
195             ?: Log.w(
196                 ConfigurationManager.TAG,
197                 "No intent available for checking cross-profile access.",
198             )
199 
200         // Nothing left to check
201         return false
202     }
203 
204     /**
205      * Checks if mimeTypes contains only video MIME types and no image MIME types.
206      *
207      * @return `true` if mimeTypes list contains only video MIME types (starting with "video/") and
208      *   no image MIME types (starting with "image/"), `false` otherwise.
209      */
hasOnlyVideoMimeTypesnull210     fun hasOnlyVideoMimeTypes(): Boolean {
211         return mimeTypes.isNotEmpty() && mimeTypes.all { it.startsWith("video/") }
212     }
213 }
214