1 /* 2 * 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 17 18 import android.content.Intent 19 import android.content.Intent.ACTION_GET_CONTENT 20 import android.os.Build.VERSION_CODES 21 import android.platform.test.annotations.RequiresFlagsEnabled 22 import android.platform.test.flag.junit.CheckFlagsRule 23 import android.platform.test.flag.junit.DeviceFlagsValueProvider 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SdkSuppress 26 import androidx.test.filters.SmallTest 27 import androidx.test.platform.app.InstrumentationRegistry 28 import androidx.test.uiautomator.By 29 import androidx.test.uiautomator.UiDevice 30 import androidx.test.uiautomator.Until 31 import com.android.documentsui.flags.Flags.FLAG_REDIRECT_GET_CONTENT_RO 32 import com.android.documentsui.picker.TrampolineActivity 33 import java.util.Optional 34 import java.util.regex.Pattern 35 import org.junit.Assert.assertNotNull 36 import org.junit.Before 37 import org.junit.BeforeClass 38 import org.junit.Rule 39 import org.junit.Test 40 import org.junit.runner.RunWith 41 import org.junit.runners.Parameterized 42 import org.junit.runners.Suite 43 import org.junit.runners.Suite.SuiteClasses 44 45 @SmallTest 46 @RunWith(Suite::class) 47 @SuiteClasses( 48 TrampolineActivityTest.ShouldLaunchCorrectPackageTest::class, 49 TrampolineActivityTest.RedirectTest::class 50 ) 51 class TrampolineActivityTest() { 52 companion object { 53 const val UI_TIMEOUT = 5000L 54 val PHOTOPICKER_PACKAGE_REGEX: Pattern = Pattern.compile(".*(photopicker|media\\.module).*") 55 val DOCUMENTSUI_PACKAGE_REGEX: Pattern = Pattern.compile(".*documentsui.*") 56 val STACK_LIST_REGEX: Pattern = Pattern.compile( 57 "taskId=(?<taskId>[0-9]+):(.+?)(photopicker|media\\.module|documentsui)", 58 Pattern.MULTILINE 59 ) 60 61 private lateinit var device: UiDevice 62 removePhotopickerAndDocumentsUITasksnull63 fun removePhotopickerAndDocumentsUITasks() { 64 // Get the current list of tasks that are visible. 65 val result = device.executeShellCommand("am stack list") 66 67 // Identify any that are from DocumentsUI or Photopicker and close them. 68 val matcher = STACK_LIST_REGEX.matcher(result) 69 while (matcher.find()) { 70 device.executeShellCommand("am stack remove ${matcher.group("taskId")}") 71 } 72 } 73 74 @BeforeClass 75 @JvmStatic setUpnull76 fun setUp() { 77 device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 78 } 79 } 80 81 @RunWith(Parameterized::class) 82 @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO) 83 class ShouldLaunchCorrectPackageTest { 84 enum class AppType { 85 PHOTOPICKER, 86 DOCUMENTSUI, 87 } 88 89 data class GetContentIntentData( 90 val mimeType: String, 91 val expectedApp: AppType, 92 val extraMimeTypes: Optional<Array<String>> = Optional.empty(), 93 ) { toStringnull94 override fun toString(): String { 95 if (extraMimeTypes.isPresent) { 96 return "${mimeType}_${extraMimeTypes.get().joinToString("_")}" 97 } 98 return mimeType 99 } 100 } 101 102 companion object { 103 @Parameterized.Parameters(name = "{0}") 104 @JvmStatic parametersnull105 fun parameters() = 106 listOf( 107 GetContentIntentData( 108 mimeType = "*/*", 109 expectedApp = AppType.DOCUMENTSUI, 110 ), 111 GetContentIntentData( 112 mimeType = "image/*", 113 expectedApp = AppType.PHOTOPICKER, 114 ), 115 GetContentIntentData( 116 mimeType = "video/*", 117 expectedApp = AppType.PHOTOPICKER, 118 ), 119 GetContentIntentData( 120 mimeType = "image/*", 121 extraMimeTypes = Optional.of(arrayOf("video/*")), 122 expectedApp = AppType.PHOTOPICKER, 123 ), 124 GetContentIntentData( 125 mimeType = "video/*", 126 extraMimeTypes = Optional.of(arrayOf("image/*")), 127 expectedApp = AppType.PHOTOPICKER, 128 ), 129 GetContentIntentData( 130 mimeType = "video/*", 131 extraMimeTypes = Optional.of(arrayOf("text/*")), 132 expectedApp = AppType.DOCUMENTSUI, 133 ), 134 GetContentIntentData( 135 mimeType = "video/*", 136 extraMimeTypes = Optional.of(arrayOf("image/*", "text/*")), 137 expectedApp = AppType.DOCUMENTSUI, 138 ), 139 GetContentIntentData( 140 mimeType = "*/*", 141 extraMimeTypes = Optional.of(arrayOf("image/*", "video/*")), 142 expectedApp = AppType.PHOTOPICKER, 143 ), 144 GetContentIntentData( 145 mimeType = "image/*", 146 extraMimeTypes = Optional.of(arrayOf()), 147 expectedApp = AppType.DOCUMENTSUI, 148 ) 149 ) 150 } 151 152 @Parameterized.Parameter(0) 153 lateinit var testData: GetContentIntentData 154 155 @get:Rule 156 val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() 157 158 @Before 159 fun setUp() { 160 removePhotopickerAndDocumentsUITasks() 161 162 val context = InstrumentationRegistry.getInstrumentation().targetContext 163 val intent = Intent(ACTION_GET_CONTENT) 164 intent.setClass(context, TrampolineActivity::class.java) 165 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 166 intent.setType(testData.mimeType) 167 if (testData.extraMimeTypes.isPresent) { 168 intent.putExtra(Intent.EXTRA_MIME_TYPES, testData.extraMimeTypes.get()) 169 } 170 171 context.startActivity(intent) 172 } 173 174 @Test testCorrectAppIsLaunchednull175 fun testCorrectAppIsLaunched() { 176 val bySelector = when (testData.expectedApp) { 177 AppType.PHOTOPICKER -> By.pkg(PHOTOPICKER_PACKAGE_REGEX) 178 else -> By.pkg(DOCUMENTSUI_PACKAGE_REGEX) 179 } 180 181 val builder = StringBuilder() 182 builder.append("Intent with mimetype ${testData.mimeType}") 183 if (testData.extraMimeTypes.isPresent) { 184 builder.append( 185 " and EXTRA_MIME_TYPES of ${ 186 testData.extraMimeTypes.get().joinToString(", ") 187 }" 188 ) 189 } 190 builder.append( 191 " didn't cause ${testData.expectedApp.name} to appear after ${UI_TIMEOUT}ms" 192 ) 193 194 assertNotNull( 195 builder.toString(), 196 device.wait(Until.findObject(bySelector), UI_TIMEOUT) 197 ) 198 } 199 } 200 201 @RunWith(AndroidJUnit4::class) 202 @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO) 203 class RedirectTest { 204 @get:Rule 205 val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() 206 207 @Before setUpnull208 fun setUp() { 209 removePhotopickerAndDocumentsUITasks() 210 } 211 212 @Test testReferredGetContentFromPhotopickerShouldNotRedirectBacknull213 fun testReferredGetContentFromPhotopickerShouldNotRedirectBack() { 214 val context = InstrumentationRegistry.getInstrumentation().targetContext 215 val intent = Intent(ACTION_GET_CONTENT) 216 intent.setClass(context, TrampolineActivity::class.java) 217 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 218 intent.setType("*/*") 219 intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*")) 220 221 context.startActivity(intent) 222 val moreButton = device.wait(Until.findObject(By.descContains("More")), UI_TIMEOUT) 223 moreButton?.click() 224 225 val browseButton = device.wait(Until.findObject(By.textContains("Browse")), UI_TIMEOUT) 226 browseButton?.click() 227 228 assertNotNull( 229 "DocumentsUI has not launched", 230 device.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT) 231 ) 232 } 233 234 @Test 235 @SdkSuppress(minSdkVersion = VERSION_CODES.S, maxSdkVersion = VERSION_CODES.S_V2) testAndroidSWithTakeoverGetContentDisabledShouldNotReferToDocumentsUInull236 fun testAndroidSWithTakeoverGetContentDisabledShouldNotReferToDocumentsUI() { 237 val context = InstrumentationRegistry.getInstrumentation().targetContext 238 val intent = Intent(ACTION_GET_CONTENT) 239 intent.setClass(context, TrampolineActivity::class.java) 240 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 241 intent.setType("image/*") 242 243 try { 244 // Disable Photopicker from taking over `ACTION_GET_CONTENT`. In this situation, it 245 // should ALWAYS defer to DocumentsUI regardless if the mimetype satisfies the 246 // conditions. 247 device.executeShellCommand( 248 "device_config put mediaprovider take_over_get_content false" 249 ) 250 context.startActivity(intent) 251 assertNotNull( 252 device.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT) 253 ) 254 } finally { 255 device.executeShellCommand( 256 "device_config delete mediaprovider take_over_get_content" 257 ) 258 } 259 } 260 } 261 } 262