• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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