1 /*
<lambda>null2  * Copyright (C) 2022 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 
17 package android.permissionui.cts
18 
19 import android.Manifest
20 import android.app.ActivityManager
21 import android.content.Context
22 import android.content.Intent
23 import android.os.Build
24 import android.provider.DeviceConfig
25 import android.safetylabel.SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED
26 import android.text.Spanned
27 import android.text.style.ClickableSpan
28 import android.util.Log
29 import android.view.View
30 import androidx.test.filters.FlakyTest
31 import androidx.test.filters.SdkSuppress
32 import androidx.test.uiautomator.By
33 import com.android.compatibility.common.util.DeviceConfigStateChangerRule
34 import com.android.compatibility.common.util.SystemUtil
35 import com.android.compatibility.common.util.SystemUtil.eventually
36 import com.android.modules.utils.build.SdkLevel
37 import org.junit.After
38 import org.junit.Assert.assertEquals
39 import org.junit.Assert.assertFalse
40 import org.junit.Assert.assertTrue
41 import org.junit.Assume
42 import org.junit.Before
43 import org.junit.Ignore
44 import org.junit.Rule
45 import org.junit.Test
46 
47 /** Permission rationale activity tests. Permission rationale is only available on U+ */
48 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
49 @FlakyTest
50 class PermissionRationaleTest : BaseUsePermissionTest() {
51 
52     private var activityManager: ActivityManager? = null
53 
54     @get:Rule
55     val deviceConfigPermissionRationaleEnabled =
56         DeviceConfigStateChangerRule(
57             context,
58             DeviceConfig.NAMESPACE_PRIVACY,
59             PERMISSION_RATIONALE_ENABLED,
60             true.toString()
61         )
62 
63     @Before
64     fun setup() {
65         Assume.assumeTrue("Permission rationale is only available on U+", SdkLevel.isAtLeastU())
66         Assume.assumeFalse(isAutomotive)
67         Assume.assumeFalse(isTv)
68         Assume.assumeFalse(isWatch)
69 
70         activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
71 
72         enableComponent(TEST_INSTALLER_ACTIVITY_COMPONENT_NAME)
73 
74         installPackageWithInstallSourceAndMetadata(APP_APK_NAME_31)
75 
76         assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, false)
77     }
78 
79     @After
80     fun disableTestInstallerActivity() {
81         disableComponent(TEST_INSTALLER_ACTIVITY_COMPONENT_NAME)
82     }
83 
84     @Test
85     fun startsPermissionRationaleActivity_failedByNullMetadata() {
86         installPackageWithInstallSourceAndNoMetadata(APP_APK_NAME_31)
87         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
88     }
89 
90     @Test
91     fun startsPermissionRationaleActivity_failedByEmptyMetadata() {
92         installPackageWithInstallSourceAndEmptyMetadata(APP_APK_NAME_31)
93         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
94     }
95 
96     @Test
97     fun startsPermissionRationaleActivity_failedByNoTopLevelVersion() {
98         installPackageWithInstallSourceAndMetadataWithoutTopLevelVersion(APP_APK_NAME_31)
99         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
100     }
101 
102     @Test
103     fun startsPermissionRationaleActivity_failedByInvalidTopLevelVersion() {
104         installPackageWithInstallSourceAndMetadataWithInvalidTopLevelVersion(APP_APK_NAME_31)
105         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
106     }
107 
108     @Test
109     fun startsPermissionRationaleActivity_failedByNoSafetyLabelVersion() {
110         installPackageWithInstallSourceAndMetadataWithoutSafetyLabelVersion(APP_APK_NAME_31)
111         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
112     }
113 
114     @Test
115     fun startsPermissionRationaleActivity_failedByInvalidSafetyLabelVersion() {
116         installPackageWithInstallSourceAndMetadataWithInvalidSafetyLabelVersion(APP_APK_NAME_31)
117         navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer()
118     }
119 
120     @Test
121     fun startsPermissionRationaleActivity() {
122         navigateToPermissionRationaleActivity()
123 
124         assertPermissionRationaleDialogIsVisible(true)
125     }
126 
127     @Test
128     fun linksToInstallSource() {
129         navigateToPermissionRationaleActivity()
130 
131         assertPermissionRationaleDialogIsVisible(true)
132 
133         clickInstallSourceLink()
134 
135         eventually {
136             assertStoreLinkClickSuccessful(installerPackageName = TEST_INSTALLER_PACKAGE_NAME)
137         }
138     }
139 
140     @Ignore("b/282063206")
141     @Test
142     fun clickLinkToHelpCenter_opensHelpCenter() {
143         Assume.assumeFalse(getPermissionControllerResString(HELP_CENTER_URL_ID).isNullOrEmpty())
144 
145         navigateToPermissionRationaleActivity()
146 
147         assertPermissionRationaleActivityTitleIsVisible(true)
148         assertHelpCenterLinkAvailable(true)
149 
150         clickHelpCenterLink()
151 
152         eventually({ assertHelpCenterLinkClickSuccessful() }, NEW_WINDOW_TIMEOUT_MILLIS)
153     }
154 
155     @Test
156     fun noHelpCenterLinkAvailable_noHelpCenterClickAction() {
157         Assume.assumeTrue(getPermissionControllerResString(HELP_CENTER_URL_ID).isNullOrEmpty())
158 
159         navigateToPermissionRationaleActivity()
160 
161         assertPermissionRationaleActivityTitleIsVisible(true)
162         assertHelpCenterLinkAvailable(false)
163     }
164 
165     @Test
166     fun linksToSettings_noOp_dialogsNotClosed() {
167         navigateToPermissionRationaleActivity()
168 
169         assertPermissionRationaleDialogIsVisible(true)
170 
171         clicksSettings_doesNothing_leaves()
172 
173         eventually { assertPermissionRationaleDialogIsVisible(true) }
174     }
175 
176     @Test
177     fun linksToSettings_grants_dialogsClose() {
178         navigateToPermissionRationaleActivity()
179 
180         assertPermissionRationaleDialogIsVisible(true)
181 
182         clicksSettings_allowsForeground_leaves()
183 
184         // Setting, Permission rationale and Grant dialog should be dismissed
185         eventually {
186             assertPermissionSettingsVisible(false)
187             assertPermissionRationaleDialogIsVisible(false)
188             assertPermissionRationaleContainerOnGrantDialogIsVisible(false)
189         }
190 
191         assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, true)
192     }
193 
194     @Test
195     fun linksToSettings_denies_dialogsClose() {
196         navigateToPermissionRationaleActivity()
197 
198         assertPermissionRationaleDialogIsVisible(true)
199 
200         clicksSettings_denies_leaves()
201 
202         // Setting, Permission rationale and Grant dialog should be dismissed
203         eventually {
204             assertPermissionSettingsVisible(false)
205             assertPermissionRationaleDialogIsVisible(false)
206             assertPermissionRationaleContainerOnGrantDialogIsVisible(false)
207         }
208 
209         assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, false)
210     }
211 
212     private fun navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() {
213         requestAppPermissionsForNoResult(Manifest.permission.ACCESS_FINE_LOCATION) {
214             assertPermissionRationaleContainerOnGrantDialogIsVisible(false)
215         }
216     }
217 
218     private fun navigateToPermissionRationaleActivity() {
219         requestAppPermissionsForNoResult(Manifest.permission.ACCESS_FINE_LOCATION) {
220             assertPermissionRationaleContainerOnGrantDialogIsVisible(true)
221             clickPermissionRationaleViewInGrantDialog()
222         }
223     }
224 
225     private fun clickInstallSourceLink() {
226         findView(By.res(DATA_SHARING_SOURCE_MESSAGE_ID), true)
227 
228         eventually {
229             // UiObject2 doesn't expose CharSequence.
230             val node =
231                 uiAutomation.rootInActiveWindow
232                     .findAccessibilityNodeInfosByViewId(DATA_SHARING_SOURCE_MESSAGE_ID)[0]
233             assertTrue(node.isVisibleToUser)
234             val text = node.text as Spanned
235             val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
236             // We could pass in null here in Java, but we need an instance in Kotlin.
237             doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) }
238         }
239     }
240 
241     private fun clickHelpCenterLink() {
242         findView(By.res(LEARN_MORE_MESSAGE_ID), true)
243 
244         eventually {
245             // UiObject2 doesn't expose CharSequence.
246             val node =
247                 uiAutomation.rootInActiveWindow
248                     .findAccessibilityNodeInfosByViewId(LEARN_MORE_MESSAGE_ID)[0]
249             assertTrue(node.isVisibleToUser)
250             val text = node.text as Spanned
251             val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
252             // We could pass in null here in Java, but we need an instance in Kotlin.
253             doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) }
254         }
255     }
256 
257     private fun clickSettingsLink() {
258         findView(By.res(SETTINGS_MESSAGE_ID), true)
259 
260         eventually {
261             // UiObject2 doesn't expose CharSequence.
262             val node =
263                 uiAutomation.rootInActiveWindow
264                     .findAccessibilityNodeInfosByViewId(SETTINGS_MESSAGE_ID)[0]
265             assertTrue(node.isVisibleToUser)
266             val text = node.text as Spanned
267             val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
268             // We could pass in null here in Java, but we need an instance in Kotlin.
269             doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) }
270         }
271     }
272 
273     private fun clicksSettings_doesNothing_leaves() {
274         clickSettingsLink()
275         eventually { assertPermissionSettingsVisible(true) }
276         pressBack()
277     }
278 
279     private fun clicksSettings_allowsForeground_leaves() {
280         clickSettingsLink()
281         eventually { clickAllowForegroundInSettings() }
282         pressBack()
283     }
284 
285     private fun clicksSettings_denies_leaves() {
286         clickSettingsLink()
287         eventually { clicksDenyInSettings() }
288         pressBack()
289     }
290 
291     private fun assertHelpCenterLinkAvailable(expected: Boolean) {
292         // Message should always be visible
293         findView(By.res(LEARN_MORE_MESSAGE_ID), true)
294 
295         // Verify the link is (or isn't) in message
296         eventually {
297             // UiObject2 doesn't expose CharSequence.
298             val node =
299                 uiAutomation.rootInActiveWindow
300                     .findAccessibilityNodeInfosByViewId(LEARN_MORE_MESSAGE_ID)[0]
301             assertTrue(node.isVisibleToUser)
302             val text = node.text as Spanned
303             val clickableSpans = text.getSpans(0, text.length, ClickableSpan::class.java)
304 
305             if (expected) {
306                 assertFalse("Expected help center link, but none found", clickableSpans.isEmpty())
307             } else {
308                 assertTrue("Expected no links, but found one", clickableSpans.isEmpty())
309             }
310         }
311     }
312 
313     private fun assertPermissionSettingsVisible(expected: Boolean) {
314         findView(By.res(DENY_RADIO_BUTTON), expected = expected)
315     }
316 
317     private fun assertStoreLinkClickSuccessful(
318         installerPackageName: String,
319         packageName: String? = null
320     ) {
321         SystemUtil.runWithShellPermissionIdentity {
322             val runningTasks = activityManager!!.getRunningTasks(1)
323 
324             assertFalse("Expected runningTasks to not be empty", runningTasks.isEmpty())
325 
326             val taskInfo = runningTasks[0]
327             val observedIntentAction = taskInfo.baseIntent.action
328             val observedPackageName = taskInfo.baseIntent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
329             val observedInstallerPackageName = taskInfo.topActivity?.packageName
330 
331             assertEquals(
332                 "Unexpected intent action",
333                 Intent.ACTION_SHOW_APP_INFO,
334                 observedIntentAction
335             )
336             assertEquals(
337                 "Unexpected installer package name",
338                 installerPackageName,
339                 observedInstallerPackageName
340             )
341             assertEquals("Unexpected package name", packageName, observedPackageName)
342         }
343     }
344 
345     private fun assertHelpCenterLinkClickSuccessful() {
346         SystemUtil.runWithShellPermissionIdentity {
347             val runningTasks = activityManager!!.getRunningTasks(5)
348 
349             Log.v(TAG, "# running tasks: ${runningTasks.size}")
350             assertFalse("Expected runningTasks to not be empty", runningTasks.isEmpty())
351 
352             runningTasks.forEachIndexed { index, runningTaskInfo ->
353                 Log.v(TAG, "task $index ${runningTaskInfo.baseIntent}")
354             }
355 
356             val taskInfo = runningTasks[0]
357             val observedIntentAction = taskInfo.baseIntent.action
358             val observedIntentDataString = taskInfo.baseIntent.dataString
359             val observedIntentScheme: String? = taskInfo.baseIntent.scheme
360 
361             Log.v(TAG, "task base intent: ${taskInfo.baseIntent}")
362             assertEquals("Unexpected intent action", Intent.ACTION_VIEW, observedIntentAction)
363 
364             val expectedUrl = getPermissionControllerResString(HELP_CENTER_URL_ID)!!
365             assertFalse(observedIntentDataString.isNullOrEmpty())
366             assertTrue(observedIntentDataString?.startsWith(expectedUrl) ?: false)
367 
368             assertFalse(observedIntentScheme.isNullOrEmpty())
369             assertEquals("https", observedIntentScheme)
370         }
371     }
372 
373     companion object {
374         private val TAG = PermissionRationaleTest::class.java.simpleName
375 
376         private const val DATA_SHARING_SOURCE_MESSAGE_ID =
377             "com.android.permissioncontroller:id/data_sharing_source_message"
378         private const val LEARN_MORE_MESSAGE_ID =
379             "com.android.permissioncontroller:id/learn_more_message"
380         private const val SETTINGS_MESSAGE_ID =
381             "com.android.permissioncontroller:id/settings_message"
382 
383         private const val HELP_CENTER_URL_ID = "data_sharing_help_center_link"
384     }
385 }
386