Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa560a97 authored by Ben Reich's avatar Ben Reich Committed by Android (Google) Code Review
Browse files

Merge "Fix TrampolineActivityTest on Android T" into main

parents 7f41f74b f40eb8e3
Loading
Loading
Loading
Loading
+5 −80
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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
+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
    }
}
+13 −7
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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 " +
@@ -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)