Loading src/com/android/documentsui/picker/TrampolineActivity.kt +5 −80 Original line number Diff line number Diff line Loading @@ -15,18 +15,11 @@ */ package com.android.documentsui.picker import android.content.ComponentName import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.os.ext.SdkExtensions import android.provider.MediaStore.ACTION_PICK_IMAGES import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.android.documentsui.base.SharedMinimal.DEBUG import com.android.documentsui.util.getPhotopickerGetContentComponentNameForType /** * DocumentsUI PickActivity currently defers picking of media mime types to the Photopicker. This Loading @@ -49,7 +42,8 @@ class TrampolineActivity : AppCompatActivity() { } // In the event there is no photopicker returned, just refer to DocumentsUI. val photopickerComponentName = getPhotopickerComponentName(intent.type) val photopickerComponentName = getPhotopickerGetContentComponentNameForType(packageManager, intent.type) if (photopickerComponentName == null) { forwardIntentToDocumentsUI() return Loading @@ -74,78 +68,9 @@ class TrampolineActivity : AppCompatActivity() { startActivity(intent) finish() } private fun getPhotopickerComponentName(type: String?): ComponentName? { // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that // the Photopicker was not available, so in those cases should always send to DocumentsUI. if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) { return null } // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package. // On T+ devices this is is a standalone package, whilst prior to T it is part of the // MediaProvider module. val pickImagesIntent = Intent( ACTION_PICK_IMAGES ).apply { addCategory(Intent.CATEGORY_DEFAULT) } val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity( packageManager ) // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES. val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply { setType(type) setPackage(photopickerComponentName?.packageName) } val photopickerGetContentComponent: ComponentName? = photopickerGetContentIntent.resolveActivity(packageManager) // Ensure the `ACTION_GET_CONTENT` activity is enabled. if (!isComponentEnabled(photopickerGetContentComponent)) { if (DEBUG) { Log.d(TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler") } return null } return photopickerGetContentComponent } private fun isComponentEnabled(componentName: ComponentName?): Boolean { if (componentName == null) { return false } return when (packageManager.getComponentEnabledSetting(componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { // DEFAULT is a state that essentially defers to the state defined in the // AndroidManifest which can be either enabled or disabled. packageManager.getPackageInfo( componentName.packageName, PackageManager.GET_ACTIVITIES )?.let { packageInfo: PackageInfo -> if (packageInfo.activities == null) { return false } for (val info in packageInfo.activities) { if (info.name == componentName.className) { return info.enabled } } } return false } // Everything else is considered disabled. else -> false } } } fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { private fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { // Photopicker can only handle `ACTION_GET_CONTENT` intents. if (intent.action != ACTION_GET_CONTENT) { return false Loading Loading @@ -174,7 +99,7 @@ fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { return extraMimeTypes.isNotEmpty() && extraMimeTypes.none { !isMediaMimeType(it) } } fun isMediaMimeType(mimeType: String?): Boolean { private fun isMediaMimeType(mimeType: String?): Boolean { return mimeType?.let { mimeType -> mimeType.startsWith("image/") || mimeType.startsWith("video/") } == true Loading src/com/android/documentsui/util/ComponentUtils.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.util import android.content.ComponentName import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import android.os.ext.SdkExtensions import android.provider.MediaStore.ACTION_PICK_IMAGES import android.util.Log import com.android.documentsui.base.SharedMinimal.DEBUG import com.android.documentsui.picker.TrampolineActivity /** * Returns the ComponentName for the Photopicker on the device that handles the GET_CONTENT action. * Uses the PICK_IMAGES action to get the proper component name then attempts to find the * GET_CONTENT handler for that explicit component. */ fun getPhotopickerGetContentComponentNameForType( packageManager: PackageManager, type: String? ): ComponentName? { // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that // the Photopicker was not available, so in those cases should always send to DocumentsUI. if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) { return null } // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package. // On T+ devices this is is a standalone package, whilst prior to T it is part of the // MediaProvider module. val pickImagesIntent = Intent( ACTION_PICK_IMAGES ).apply { addCategory(Intent.CATEGORY_DEFAULT) } val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity( packageManager ) // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES. val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply { setType(type) setPackage(photopickerComponentName?.packageName) } val photopickerGetContentComponent: ComponentName? = photopickerGetContentIntent.resolveActivity(packageManager) // Ensure the `ACTION_GET_CONTENT` activity is enabled. if (!isComponentEnabled(packageManager, photopickerGetContentComponent)) { if (DEBUG) { Log.d( TrampolineActivity.Companion.TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler" ) } return null } return photopickerGetContentComponent } /** * Private method to check if the supplied ComponentName is enabled or not. * Photopicker dynamically disables itself in some instances. */ private fun isComponentEnabled( packageManager: PackageManager, componentName: ComponentName? ): Boolean { if (componentName == null) { return false } return when (packageManager.getComponentEnabledSetting(componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { // DEFAULT is a state that essentially defers to the state defined in the // AndroidManifest which can be either enabled or disabled. packageManager.getPackageInfo( componentName.packageName, PackageManager.GET_ACTIVITIES )?.let { packageInfo: PackageInfo -> if (packageInfo.activities == null) { return false } for (val info in packageInfo.activities) { if (info.name == componentName.className) { return info.enabled } } } return false } // Everything else is considered disabled. else -> false } } tests/functional/com/android/documentsui/TrampolineActivityTest.kt +13 −7 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package com.android.documentsui import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.os.Build.VERSION_CODES import android.os.ext.SdkExtensions import android.platform.test.annotations.RequiresFlagsEnabled import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress Loading @@ -30,6 +29,8 @@ import androidx.test.uiautomator.Until import com.android.documentsui.flags.Flags.FLAG_REDIRECT_GET_CONTENT_RO import com.android.documentsui.picker.TrampolineActivity import com.android.documentsui.rules.CheckAndForceMaterial3Flag import com.android.documentsui.util.getPhotopickerGetContentComponentNameForType import com.google.common.truth.TruthJUnit.assume import java.util.Optional import java.util.regex.Pattern import kotlin.time.Duration.Companion.seconds Loading Loading @@ -200,12 +201,12 @@ class TrampolineActivityTest() { context.startActivity(intent) // Photopicker was introduced into the platform in Android T, however it was backported // to R via SdkExtensions in MediaProvider. Ensure that the target device has the // backport otherwise fallback to DocumentsUI. val isPhotopickerAvailable = SdkExtensions.getExtensionVersion(VERSION_CODES.R) >= 2 val isPhotopickerGetContentComponentAvailable = getPhotopickerGetContentComponentNameForType( context.packageManager, testData.mimeType) != null val bySelector = when { testData.expectedApp == AppType.PHOTOPICKER && isPhotopickerAvailable -> By.pkg( testData.expectedApp == AppType.PHOTOPICKER && isPhotopickerGetContentComponentAvailable -> By.pkg( PHOTOPICKER_PACKAGE_REGEX ) else -> By.pkg(DOCUMENTSUI_PACKAGE_REGEX) Loading @@ -224,7 +225,8 @@ class TrampolineActivityTest() { " and EXTRA_MIME_TYPES of ($extraMimeTypes)" ) } if (testData.expectedApp == AppType.PHOTOPICKER && !isPhotopickerAvailable) { if (testData.expectedApp == AppType.PHOTOPICKER && !isPhotopickerGetContentComponentAvailable) { builder.append( " didn't cause ${AppType.DOCUMENTSUI} to appear " + "(${AppType.PHOTOPICKER} is expected, but is not available in this " + Loading Loading @@ -259,6 +261,10 @@ class TrampolineActivityTest() { @Test fun testReferredGetContentFromPhotopickerShouldNotRedirectBack() { val context = InstrumentationRegistry.getInstrumentation().targetContext assume().that( getPhotopickerGetContentComponentNameForType(context.packageManager, "image/*") ).isNotNull() val intent = Intent(ACTION_GET_CONTENT) intent.setClass(context, TrampolineActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Loading Loading
src/com/android/documentsui/picker/TrampolineActivity.kt +5 −80 Original line number Diff line number Diff line Loading @@ -15,18 +15,11 @@ */ package com.android.documentsui.picker import android.content.ComponentName import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.os.ext.SdkExtensions import android.provider.MediaStore.ACTION_PICK_IMAGES import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.android.documentsui.base.SharedMinimal.DEBUG import com.android.documentsui.util.getPhotopickerGetContentComponentNameForType /** * DocumentsUI PickActivity currently defers picking of media mime types to the Photopicker. This Loading @@ -49,7 +42,8 @@ class TrampolineActivity : AppCompatActivity() { } // In the event there is no photopicker returned, just refer to DocumentsUI. val photopickerComponentName = getPhotopickerComponentName(intent.type) val photopickerComponentName = getPhotopickerGetContentComponentNameForType(packageManager, intent.type) if (photopickerComponentName == null) { forwardIntentToDocumentsUI() return Loading @@ -74,78 +68,9 @@ class TrampolineActivity : AppCompatActivity() { startActivity(intent) finish() } private fun getPhotopickerComponentName(type: String?): ComponentName? { // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that // the Photopicker was not available, so in those cases should always send to DocumentsUI. if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) { return null } // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package. // On T+ devices this is is a standalone package, whilst prior to T it is part of the // MediaProvider module. val pickImagesIntent = Intent( ACTION_PICK_IMAGES ).apply { addCategory(Intent.CATEGORY_DEFAULT) } val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity( packageManager ) // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES. val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply { setType(type) setPackage(photopickerComponentName?.packageName) } val photopickerGetContentComponent: ComponentName? = photopickerGetContentIntent.resolveActivity(packageManager) // Ensure the `ACTION_GET_CONTENT` activity is enabled. if (!isComponentEnabled(photopickerGetContentComponent)) { if (DEBUG) { Log.d(TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler") } return null } return photopickerGetContentComponent } private fun isComponentEnabled(componentName: ComponentName?): Boolean { if (componentName == null) { return false } return when (packageManager.getComponentEnabledSetting(componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { // DEFAULT is a state that essentially defers to the state defined in the // AndroidManifest which can be either enabled or disabled. packageManager.getPackageInfo( componentName.packageName, PackageManager.GET_ACTIVITIES )?.let { packageInfo: PackageInfo -> if (packageInfo.activities == null) { return false } for (val info in packageInfo.activities) { if (info.name == componentName.className) { return info.enabled } } } return false } // Everything else is considered disabled. else -> false } } } fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { private fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { // Photopicker can only handle `ACTION_GET_CONTENT` intents. if (intent.action != ACTION_GET_CONTENT) { return false Loading Loading @@ -174,7 +99,7 @@ fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { return extraMimeTypes.isNotEmpty() && extraMimeTypes.none { !isMediaMimeType(it) } } fun isMediaMimeType(mimeType: String?): Boolean { private fun isMediaMimeType(mimeType: String?): Boolean { return mimeType?.let { mimeType -> mimeType.startsWith("image/") || mimeType.startsWith("video/") } == true Loading
src/com/android/documentsui/util/ComponentUtils.kt 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.util import android.content.ComponentName import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import android.os.ext.SdkExtensions import android.provider.MediaStore.ACTION_PICK_IMAGES import android.util.Log import com.android.documentsui.base.SharedMinimal.DEBUG import com.android.documentsui.picker.TrampolineActivity /** * Returns the ComponentName for the Photopicker on the device that handles the GET_CONTENT action. * Uses the PICK_IMAGES action to get the proper component name then attempts to find the * GET_CONTENT handler for that explicit component. */ fun getPhotopickerGetContentComponentNameForType( packageManager: PackageManager, type: String? ): ComponentName? { // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that // the Photopicker was not available, so in those cases should always send to DocumentsUI. if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) { return null } // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package. // On T+ devices this is is a standalone package, whilst prior to T it is part of the // MediaProvider module. val pickImagesIntent = Intent( ACTION_PICK_IMAGES ).apply { addCategory(Intent.CATEGORY_DEFAULT) } val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity( packageManager ) // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES. val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply { setType(type) setPackage(photopickerComponentName?.packageName) } val photopickerGetContentComponent: ComponentName? = photopickerGetContentIntent.resolveActivity(packageManager) // Ensure the `ACTION_GET_CONTENT` activity is enabled. if (!isComponentEnabled(packageManager, photopickerGetContentComponent)) { if (DEBUG) { Log.d( TrampolineActivity.Companion.TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler" ) } return null } return photopickerGetContentComponent } /** * Private method to check if the supplied ComponentName is enabled or not. * Photopicker dynamically disables itself in some instances. */ private fun isComponentEnabled( packageManager: PackageManager, componentName: ComponentName? ): Boolean { if (componentName == null) { return false } return when (packageManager.getComponentEnabledSetting(componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { // DEFAULT is a state that essentially defers to the state defined in the // AndroidManifest which can be either enabled or disabled. packageManager.getPackageInfo( componentName.packageName, PackageManager.GET_ACTIVITIES )?.let { packageInfo: PackageInfo -> if (packageInfo.activities == null) { return false } for (val info in packageInfo.activities) { if (info.name == componentName.className) { return info.enabled } } } return false } // Everything else is considered disabled. else -> false } }
tests/functional/com/android/documentsui/TrampolineActivityTest.kt +13 −7 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package com.android.documentsui import android.content.Intent import android.content.Intent.ACTION_GET_CONTENT import android.os.Build.VERSION_CODES import android.os.ext.SdkExtensions import android.platform.test.annotations.RequiresFlagsEnabled import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress Loading @@ -30,6 +29,8 @@ import androidx.test.uiautomator.Until import com.android.documentsui.flags.Flags.FLAG_REDIRECT_GET_CONTENT_RO import com.android.documentsui.picker.TrampolineActivity import com.android.documentsui.rules.CheckAndForceMaterial3Flag import com.android.documentsui.util.getPhotopickerGetContentComponentNameForType import com.google.common.truth.TruthJUnit.assume import java.util.Optional import java.util.regex.Pattern import kotlin.time.Duration.Companion.seconds Loading Loading @@ -200,12 +201,12 @@ class TrampolineActivityTest() { context.startActivity(intent) // Photopicker was introduced into the platform in Android T, however it was backported // to R via SdkExtensions in MediaProvider. Ensure that the target device has the // backport otherwise fallback to DocumentsUI. val isPhotopickerAvailable = SdkExtensions.getExtensionVersion(VERSION_CODES.R) >= 2 val isPhotopickerGetContentComponentAvailable = getPhotopickerGetContentComponentNameForType( context.packageManager, testData.mimeType) != null val bySelector = when { testData.expectedApp == AppType.PHOTOPICKER && isPhotopickerAvailable -> By.pkg( testData.expectedApp == AppType.PHOTOPICKER && isPhotopickerGetContentComponentAvailable -> By.pkg( PHOTOPICKER_PACKAGE_REGEX ) else -> By.pkg(DOCUMENTSUI_PACKAGE_REGEX) Loading @@ -224,7 +225,8 @@ class TrampolineActivityTest() { " and EXTRA_MIME_TYPES of ($extraMimeTypes)" ) } if (testData.expectedApp == AppType.PHOTOPICKER && !isPhotopickerAvailable) { if (testData.expectedApp == AppType.PHOTOPICKER && !isPhotopickerGetContentComponentAvailable) { builder.append( " didn't cause ${AppType.DOCUMENTSUI} to appear " + "(${AppType.PHOTOPICKER} is expected, but is not available in this " + Loading Loading @@ -259,6 +261,10 @@ class TrampolineActivityTest() { @Test fun testReferredGetContentFromPhotopickerShouldNotRedirectBack() { val context = InstrumentationRegistry.getInstrumentation().targetContext assume().that( getPhotopickerGetContentComponentNameForType(context.packageManager, "image/*") ).isNotNull() val intent = Intent(ACTION_GET_CONTENT) intent.setClass(context, TrampolineActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Loading