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