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