1 /*
<lambda>null2 * Copyright (C) 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 package com.android.documentsui.picker
17
18 import android.content.ComponentName
19 import android.content.Intent
20 import android.content.Intent.ACTION_GET_CONTENT
21 import android.content.pm.PackageInfo
22 import android.content.pm.PackageManager
23 import android.os.Build
24 import android.os.Bundle
25 import android.os.ext.SdkExtensions
26 import android.provider.MediaStore.ACTION_PICK_IMAGES
27 import android.util.Log
28 import androidx.appcompat.app.AppCompatActivity
29 import com.android.documentsui.base.SharedMinimal.DEBUG
30
31 /**
32 * DocumentsUI PickActivity currently defers picking of media mime types to the Photopicker. This
33 * activity trampolines the intent to either Photopicker or to the PickActivity depending on whether
34 * there are non-media mime types to handle.
35 */
36 class TrampolineActivity : AppCompatActivity() {
37 companion object {
38 const val TAG = "TrampolineActivity"
39 }
40
41 override fun onCreate(savedInstanceBundle: Bundle?) {
42 super.onCreate(savedInstanceBundle)
43
44 // This activity should not be present in the back stack nor should handle any of the
45 // corresponding results when picking items.
46 intent?.apply {
47 addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
48 addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
49 }
50
51 // In the event there is no photopicker returned, just refer to DocumentsUI.
52 val photopickerComponentName = getPhotopickerComponentName(intent.type)
53 if (photopickerComponentName == null) {
54 forwardIntentToDocumentsUI()
55 return
56 }
57
58 // The Photopicker has an entry point to take them back to DocumentsUI. In the event the
59 // user originated from Photopicker, we don't want to send them back.
60 val referredFromPhotopicker = referrer?.host == photopickerComponentName.packageName
61 if (referredFromPhotopicker || !shouldForwardIntentToPhotopicker(intent)) {
62 forwardIntentToDocumentsUI()
63 return
64 }
65
66 // Forward intent to Photopicker.
67 intent.setComponent(photopickerComponentName)
68 startActivity(intent)
69 finish()
70 }
71
72 private fun forwardIntentToDocumentsUI() {
73 intent.setClass(applicationContext, PickActivity::class.java)
74 startActivity(intent)
75 finish()
76 }
77
78 private fun getPhotopickerComponentName(type: String?): ComponentName? {
79 // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that
80 // the Photopicker was not available, so in those cases should always send to DocumentsUI.
81 if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) {
82 return null
83 }
84
85 // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package.
86 // On T+ devices this is is a standalone package, whilst prior to T it is part of the
87 // MediaProvider module.
88 val pickImagesIntent = Intent(
89 ACTION_PICK_IMAGES
90 ).apply { addCategory(Intent.CATEGORY_DEFAULT) }
91 val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity(
92 packageManager
93 )
94
95 // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when
96 // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the
97 // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES.
98 val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply {
99 setType(type)
100 setPackage(photopickerComponentName?.packageName)
101 }
102 val photopickerGetContentComponent: ComponentName? =
103 photopickerGetContentIntent.resolveActivity(packageManager)
104
105 // Ensure the `ACTION_GET_CONTENT` activity is enabled.
106 if (!isComponentEnabled(photopickerGetContentComponent)) {
107 if (DEBUG) {
108 Log.d(TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler")
109 }
110 return null
111 }
112
113 return photopickerGetContentComponent
114 }
115
116 private fun isComponentEnabled(componentName: ComponentName?): Boolean {
117 if (componentName == null) {
118 return false
119 }
120
121 return when (packageManager.getComponentEnabledSetting(componentName)) {
122 PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
123 PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> {
124 // DEFAULT is a state that essentially defers to the state defined in the
125 // AndroidManifest which can be either enabled or disabled.
126 packageManager.getPackageInfo(
127 componentName.packageName,
128 PackageManager.GET_ACTIVITIES
129 )?.let { packageInfo: PackageInfo ->
130 if (packageInfo.activities == null) {
131 return false
132 }
133 for (val info in packageInfo.activities) {
134 if (info.name == componentName.className) {
135 return info.enabled
136 }
137 }
138 }
139 return false
140 }
141
142 // Everything else is considered disabled.
143 else -> false
144 }
145 }
146 }
147
shouldForwardIntentToPhotopickernull148 fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean {
149 // Photopicker can only handle `ACTION_GET_CONTENT` intents.
150 if (intent.action != ACTION_GET_CONTENT) {
151 return false
152 }
153
154 // Photopicker only handles media mime types (i.e. image/* or video/*), however, it also handles
155 // requests that have type */* with EXTRA_MIME_TYPES that are media mime types. In that scenario
156 // it provides an escape hatch to the user to go back to DocumentsUI.
157 val intentTypeIsMedia = isMediaMimeType(intent.type)
158 if (!intentTypeIsMedia && intent.type != "*/*") {
159 return false
160 }
161
162 val extraMimeTypes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
163
164 // In the event there were no `EXTRA_MIME_TYPES` this should exclusively be handled by
165 // DocumentsUI and not Photopicker.
166 if (intent.type == "*/*" && extraMimeTypes == null) {
167 return false
168 }
169
170 if (extraMimeTypes == null) {
171 return intentTypeIsMedia
172 }
173
174 return extraMimeTypes.isNotEmpty() && extraMimeTypes.none { !isMediaMimeType(it) }
175 }
176
isMediaMimeTypenull177 fun isMediaMimeType(mimeType: String?): Boolean {
178 return mimeType?.let { mimeType ->
179 mimeType.startsWith("image/") || mimeType.startsWith("video/")
180 } == true
181 }
182