1 /*
<lambda>null2  * Copyright (C) 2016 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.permission3.cts
18 
19 import android.app.Activity
20 import android.app.Instrumentation
21 import android.content.ComponentName
22 import android.content.Intent
23 import android.content.pm.PackageManager
24 import android.net.Uri
25 import android.os.Build
26 import android.provider.Settings
27 import android.support.test.uiautomator.By
28 import android.support.test.uiautomator.BySelector
29 import android.support.test.uiautomator.UiScrollable
30 import android.support.test.uiautomator.UiSelector
31 import android.support.test.uiautomator.StaleObjectException
32 import android.text.Spanned
33 import android.text.style.ClickableSpan
34 import android.util.Log
35 import android.view.View
36 import com.android.compatibility.common.util.SystemUtil.eventually
37 import org.junit.After
38 import org.junit.Assert.assertEquals
39 import org.junit.Assert.assertNotNull
40 import org.junit.Assert.assertTrue
41 import org.junit.Before
42 import java.util.concurrent.TimeUnit
43 import java.util.regex.Pattern
44 
45 abstract class BaseUsePermissionTest : BasePermissionTest() {
46     companion object {
47         const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk"
48         const val APP_APK_PATH_22_CALENDAR_ONLY =
49             "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk"
50         const val APP_APK_PATH_22_NONE = "$APK_DIRECTORY/CtsUsePermissionApp22None.apk"
51         const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk"
52         const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk"
53         const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk"
54         const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk"
55         const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk"
56         const val APP_APK_PATH_30 = "$APK_DIRECTORY/CtsUsePermissionApp30.apk"
57         const val APP_APK_PATH_30_WITH_BACKGROUND =
58                 "$APK_DIRECTORY/CtsUsePermissionApp30WithBackground.apk"
59         const val APP_APK_PATH_30_WITH_BLUETOOTH =
60                 "$APK_DIRECTORY/CtsUsePermissionApp30WithBluetooth.apk"
61         const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk"
62         const val APP_APK_PATH_LATEST_NONE = "$APK_DIRECTORY/CtsUsePermissionAppLatestNone.apk"
63         const val APP_APK_PATH_WITH_OVERLAY = "$APK_DIRECTORY/CtsUsePermissionAppWithOverlay.apk"
64         const val APP_PACKAGE_NAME = "android.permission3.cts.usepermission"
65 
66         const val ALLOW_BUTTON =
67                 "com.android.permissioncontroller:id/permission_allow_button"
68         const val ALLOW_FOREGROUND_BUTTON =
69                 "com.android.permissioncontroller:id/permission_allow_foreground_only_button"
70         const val DENY_BUTTON = "com.android.permissioncontroller:id/permission_deny_button"
71         const val DENY_AND_DONT_ASK_AGAIN_BUTTON =
72                 "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button"
73         const val NO_UPGRADE_BUTTON =
74                 "com.android.permissioncontroller:id/permission_no_upgrade_button"
75         const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON =
76                 "com.android.permissioncontroller:" +
77                         "id/permission_no_upgrade_and_dont_ask_again_button"
78 
79         const val ALLOW_RADIO_BUTTON = "com.android.permissioncontroller:id/allow_radio_button"
80         const val ALLOW_FOREGROUND_RADIO_BUTTON =
81                 "com.android.permissioncontroller:id/allow_foreground_only_radio_button"
82         const val ASK_RADIO_BUTTON = "com.android.permissioncontroller:id/ask_radio_button"
83         const val DENY_RADIO_BUTTON = "com.android.permissioncontroller:id/deny_radio_button"
84 
85         const val ALLOW_BUTTON_TEXT = "grant_dialog_button_allow"
86         const val ALLOW_FOREGROUND_BUTTON_TEXT = "grant_dialog_button_allow_foreground"
87         const val ALLOW_FOREGROUND_PREFERENCE_TEXT = "permission_access_only_foreground"
88         const val ASK_BUTTON_TEXT = "app_permission_button_ask"
89         const val ALLOW_ONE_TIME_BUTTON_TEXT = "grant_dialog_button_allow_one_time"
90         const val DENY_BUTTON_TEXT = "grant_dialog_button_deny"
91         const val DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT =
92                 "grant_dialog_button_deny_and_dont_ask_again"
93         const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT = "grant_dialog_button_no_upgrade"
94     }
95 
96     enum class PermissionState {
97         ALLOWED,
98         DENIED,
99         DENIED_WITH_PREJUDICE
100     }
101 
102     protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
103     protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
104     protected val isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
105 
106     private val platformResources = context.createPackageContext("android", 0).resources
107     private val permissionToLabelResNameMap = mapOf(
108             // Contacts
109             android.Manifest.permission.READ_CONTACTS
110                     to "@android:string/permgrouplab_contacts",
111             android.Manifest.permission.WRITE_CONTACTS
112                     to "@android:string/permgrouplab_contacts",
113             // Calendar
114             android.Manifest.permission.READ_CALENDAR
115                     to "@android:string/permgrouplab_calendar",
116             android.Manifest.permission.WRITE_CALENDAR
117                     to "@android:string/permgrouplab_calendar",
118             // SMS
119             android.Manifest.permission.SEND_SMS to "@android:string/permgrouplab_sms",
120             android.Manifest.permission.RECEIVE_SMS to "@android:string/permgrouplab_sms",
121             android.Manifest.permission.READ_SMS to "@android:string/permgrouplab_sms",
122             android.Manifest.permission.RECEIVE_WAP_PUSH to "@android:string/permgrouplab_sms",
123             android.Manifest.permission.RECEIVE_MMS to "@android:string/permgrouplab_sms",
124             "android.permission.READ_CELL_BROADCASTS" to "@android:string/permgrouplab_sms",
125             // Storage
126             android.Manifest.permission.READ_EXTERNAL_STORAGE
127                     to "@android:string/permgrouplab_storage",
128             android.Manifest.permission.WRITE_EXTERNAL_STORAGE
129                     to "@android:string/permgrouplab_storage",
130             // Location
131             android.Manifest.permission.ACCESS_FINE_LOCATION
132                     to "@android:string/permgrouplab_location",
133             android.Manifest.permission.ACCESS_COARSE_LOCATION
134                     to "@android:string/permgrouplab_location",
135             // Phone
136             android.Manifest.permission.READ_PHONE_STATE
137                     to "@android:string/permgrouplab_phone",
138             android.Manifest.permission.CALL_PHONE to "@android:string/permgrouplab_phone",
139             "android.permission.ACCESS_IMS_CALL_SERVICE"
140                     to "@android:string/permgrouplab_phone",
141             android.Manifest.permission.READ_CALL_LOG to "@android:string/permgrouplab_phone",
142             android.Manifest.permission.WRITE_CALL_LOG to "@android:string/permgrouplab_phone",
143             android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permgrouplab_phone",
144             android.Manifest.permission.USE_SIP to "@android:string/permgrouplab_phone",
145             android.Manifest.permission.PROCESS_OUTGOING_CALLS
146                     to "@android:string/permgrouplab_phone",
147             // Microphone
148             android.Manifest.permission.RECORD_AUDIO
149                     to "@android:string/permgrouplab_microphone",
150             // Camera
151             android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera",
152             // Body sensors
153             android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors",
154             // Bluetooth
155             android.Manifest.permission.BLUETOOTH_CONNECT to
156                     "@android:string/permgrouplab_nearby_devices",
157             android.Manifest.permission.BLUETOOTH_SCAN to
158                     "@android:string/permgrouplab_nearby_devices"
159     )
160 
161     @Before
162     @After
163     fun uninstallApp() {
164         uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false)
165     }
166 
167     protected fun clearTargetSdkWarning() =
168         click(By.res("android:id/button1"))
169 
170     protected fun clickPermissionReviewContinue() {
171         if (isAutomotive || isWatch) {
172             click(By.text(getPermissionControllerString("review_button_continue")))
173         } else {
174             click(By.res("com.android.permissioncontroller:id/continue_button"))
175         }
176     }
177 
178     protected fun clickPermissionReviewCancel() {
179         if (isAutomotive || isWatch) {
180             click(By.text(getPermissionControllerString("review_button_cancel")))
181         } else {
182             click(By.res("com.android.permissioncontroller:id/cancel_button"))
183         }
184     }
185 
186     protected fun approvePermissionReview() {
187         startAppActivityAndAssertResultCode(Activity.RESULT_OK) {
188             clickPermissionReviewContinue()
189         }
190     }
191 
192     protected fun cancelPermissionReview() {
193         startAppActivityAndAssertResultCode(Activity.RESULT_CANCELED) {
194             clickPermissionReviewCancel()
195         }
196     }
197 
198     protected fun assertAppDoesNotNeedPermissionReview() {
199         startAppActivityAndAssertResultCode(Activity.RESULT_OK) {}
200     }
201 
202     protected inline fun startAppActivityAndAssertResultCode(
203         expectedResultCode: Int,
204         block: () -> Unit
205     ) {
206         val future = startActivityForFuture(
207             Intent().apply {
208                 component = ComponentName(
209                     APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity"
210                 )
211             }
212         )
213         block()
214         assertEquals(
215             expectedResultCode, future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).resultCode
216         )
217     }
218 
219     protected inline fun requestAppPermissionsForNoResult(
220         vararg permissions: String?,
221         block: () -> Unit
222     ) {
223         // Request the permissions
224         context.startActivity(
225                 Intent().apply {
226                     component = ComponentName(
227                             APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.RequestPermissionsActivity"
228                     )
229                     putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions)
230                     addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
231                 }
232         )
233         waitForIdle()
234         // Perform the post-request action
235         block()
236     }
237 
238     protected inline fun requestAppPermissions(
239         vararg permissions: String?,
240         block: () -> Unit
241     ): Instrumentation.ActivityResult {
242         // Request the permissions
243         val future = startActivityForFuture(
244             Intent().apply {
245                 component = ComponentName(
246                     APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.RequestPermissionsActivity"
247                 )
248                 putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions)
249             }
250         )
251         waitForIdle()
252         // Perform the post-request action
253         block()
254         return future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
255     }
256 
257     protected inline fun requestAppPermissionsAndAssertResult(
258         permissions: Array<out String?>,
259         permissionAndExpectedGrantResults: Array<out Pair<String?, Boolean>>,
260         block: () -> Unit
261     ) {
262         val result = requestAppPermissions(*permissions, block = block)
263         assertEquals(Activity.RESULT_OK, result.resultCode)
264         assertEquals(
265             result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!.size,
266             result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.size
267         )
268 
269         assertEquals(
270             permissionAndExpectedGrantResults.toList(),
271             result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!
272                 .zip(
273                     result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!
274                         .map { it == PackageManager.PERMISSION_GRANTED }
275                 )
276         )
277         permissionAndExpectedGrantResults.forEach {
278             it.first?.let { permission ->
279                 assertAppHasPermission(permission, it.second)
280             }
281         }
282     }
283 
284     protected inline fun requestAppPermissionsAndAssertResult(
285         vararg permissionAndExpectedGrantResults: Pair<String?, Boolean>,
286         block: () -> Unit
287     ) = requestAppPermissionsAndAssertResult(
288         permissionAndExpectedGrantResults.map { it.first }.toTypedArray(),
289         permissionAndExpectedGrantResults,
290         block
291     )
292 
293     protected fun clickPermissionRequestAllowButton() {
294         if (isAutomotive) {
295             click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)))
296         } else {
297             click(By.res(ALLOW_BUTTON))
298         }
299     }
300 
301     protected fun clickPermissionRequestSettingsLinkAndAllowAlways() {
302         clickPermissionRequestSettingsLink()
303         eventually({
304             clickAllowAlwaysInSettings()
305         }, TIMEOUT_MILLIS * 2)
306         pressBack()
307     }
308 
309     protected fun clickAllowAlwaysInSettings() {
310         if (isAutomotive || isTv || isWatch) {
311             click(By.text(getPermissionControllerString("app_permission_button_allow_always")))
312         } else {
313             click(By.res("com.android.permissioncontroller:id/allow_always_radio_button"))
314         }
315     }
316 
317     protected fun clickPermissionRequestAllowForegroundButton(timeoutMillis: Long = 10_000) {
318         if (isAutomotive) {
319             click(By.text(
320                     getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)), timeoutMillis)
321         } else {
322             click(By.res(ALLOW_FOREGROUND_BUTTON), timeoutMillis)
323         }
324     }
325 
326     protected fun clickPermissionRequestDenyButton() {
327         if (isAutomotive || isWatch || isTv) {
328             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)))
329         } else {
330             click(By.res(DENY_BUTTON))
331         }
332     }
333 
334     protected fun clickPermissionRequestSettingsLinkAndDeny() {
335         clickPermissionRequestSettingsLink()
336         if (isAutomotive || isWatch) {
337             click(By.text(getPermissionControllerString("app_permission_button_deny")))
338         } else {
339             click(By.res("com.android.permissioncontroller:id/deny_radio_button"))
340         }
341         waitForIdle()
342         pressBack()
343     }
344 
345     protected fun clickPermissionRequestSettingsLink() {
346         waitForIdle()
347         eventually {
348             // UiObject2 doesn't expose CharSequence.
349             val node = if (isAutomotive) {
350                 uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByText(
351                         "Allow in settings."
352                 )[0]
353             } else {
354                 uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByViewId(
355                         "com.android.permissioncontroller:id/detail_message"
356                 )[0]
357             }
358             assertTrue(node.isVisibleToUser)
359             val text = node.text as Spanned
360             val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
361             // We could pass in null here in Java, but we need an instance in Kotlin.
362             clickableSpan.onClick(View(context))
363         }
364         waitForIdle()
365     }
366 
367     protected fun clickPermissionRequestDenyAndDontAskAgainButton() {
368         if (isAutomotive) {
369             click(By.text(getPermissionControllerString(DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT)))
370         } else if (isWatch) {
371             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)))
372         } else {
373             click(By.res(DENY_AND_DONT_ASK_AGAIN_BUTTON))
374         }
375     }
376 
377     // Only used in TV and Watch form factors
378     protected fun clickPermissionRequestDontAskAgainButton() {
379         if (isWatch) {
380             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)))
381         } else {
382             click(
383                 By.res("com.android.permissioncontroller:id/permission_deny_dont_ask_again_button")
384             )
385         }
386     }
387 
388     protected fun clickPermissionRequestNoUpgradeAndDontAskAgainButton() {
389         if (isAutomotive) {
390             click(By.text(getPermissionControllerString(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT)))
391         } else {
392             click(By.res(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON))
393         }
394     }
395 
396     protected fun grantAppPermissions(vararg permissions: String, targetSdk: Int = 30) {
397         setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false,
398                 targetSdk = targetSdk)
399     }
400 
401     protected fun revokeAppPermissions(
402         vararg permissions: String,
403         isLegacyApp: Boolean = false,
404         targetSdk: Int = 30
405     ) {
406         setAppPermissionState(*permissions, state = PermissionState.DENIED,
407                 isLegacyApp = isLegacyApp, targetSdk = targetSdk)
408     }
409 
410     private fun setAppPermissionState(
411         vararg permissions: String,
412         state: PermissionState,
413         isLegacyApp: Boolean,
414         targetSdk: Int
415     ) {
416         if (isTv) {
417             // Dismiss DeprecatedTargetSdkVersionDialog, if present
418             if (waitFindObjectOrNull(By.text(APP_PACKAGE_NAME), 1000L) != null) {
419                 pressBack()
420             }
421             pressHome()
422         } else {
423             pressBack()
424             pressBack()
425             pressBack()
426         }
427 
428         // Try multiple times as the AppInfo page might have read stale data
429         eventually({
430             try {
431                 // Open the app details settings
432                 context.startActivity(
433                         Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
434                             data = Uri.fromParts("package", APP_PACKAGE_NAME, null)
435                             addCategory(Intent.CATEGORY_DEFAULT)
436                             addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
437                             addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
438                         }
439                 )
440                 // Open the permissions UI
441                 click(byTextRes(R.string.permissions).enabled(true))
442             } catch (e: Exception) {
443                 pressBack()
444                 throw e
445             }
446         }, TIMEOUT_MILLIS)
447 
448         for (permission in permissions) {
449             // Find the permission screen
450             val permissionLabel = getPermissionLabel(permission)
451             if (isWatch) {
452                 click(By.text(permissionLabel), 40_000)
453             } else {
454                 clickPermissionControllerUi(By.text(permissionLabel))
455             }
456 
457             val wasGranted = if (isAutomotive) {
458                 // Automotive doesn't support one time permissions, and thus
459                 // won't show an "Ask every time" message
460                 !waitFindObject(By.text(
461                         getPermissionControllerString("app_permission_button_deny"))).isChecked
462             } else if (isTv || isWatch) {
463                 !(waitFindObject(
464                     By.text(getPermissionControllerString(DENY_BUTTON_TEXT))).isChecked ||
465                     (!isLegacyApp && hasAskButton(permission) && waitFindObject(
466                         By.text(getPermissionControllerString(ASK_BUTTON_TEXT))).isChecked))
467             } else {
468                 !(waitFindObject(By.res(DENY_RADIO_BUTTON)).isChecked ||
469                     (!isLegacyApp && hasAskButton(permission) &&
470                         waitFindObject(By.res(ASK_RADIO_BUTTON)).isChecked))
471             }
472             var alreadyChecked = false
473             val button = waitFindObject(
474                 if (isAutomotive) {
475                     // Automotive doesn't support one time permissions, and thus
476                     // won't show an "Ask every time" message
477                     when (state) {
478                         PermissionState.ALLOWED ->
479                             if (showsForegroundOnlyButton(permission)) {
480                                 By.text(getPermissionControllerString(
481                                         "app_permission_button_allow_foreground"))
482                             } else {
483                                 By.text(getPermissionControllerString(
484                                                 "app_permission_button_allow"))
485                             }
486                         PermissionState.DENIED -> By.text(
487                                 getPermissionControllerString("app_permission_button_deny"))
488                         PermissionState.DENIED_WITH_PREJUDICE -> By.text(
489                                 getPermissionControllerString("app_permission_button_deny"))
490                     }
491                 } else if (isTv || isWatch) {
492                     when (state) {
493                         PermissionState.ALLOWED ->
494                             if (showsForegroundOnlyButton(permission)) {
495                                 By.text(getPermissionControllerString(
496                                         ALLOW_FOREGROUND_PREFERENCE_TEXT))
497                             } else {
498                                 By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT))
499                             }
500                         PermissionState.DENIED ->
501                             if (!isLegacyApp && hasAskButton(permission)) {
502                                 By.text(getPermissionControllerString(ASK_BUTTON_TEXT))
503                             } else {
504                                 By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
505                             }
506                         PermissionState.DENIED_WITH_PREJUDICE -> By.text(
507                                 getPermissionControllerString(DENY_BUTTON_TEXT))
508                     }
509                 } else {
510                     when (state) {
511                         PermissionState.ALLOWED ->
512                             if (showsForegroundOnlyButton(permission)) {
513                                 By.res(ALLOW_FOREGROUND_RADIO_BUTTON)
514                             } else if (isMediaStorageButton(permission, targetSdk)) {
515                                 // Uses "allow_foreground_only_radio_button" as id
516                                 byTextRes(R.string.allow_media_storage)
517                             } else if (isAllStorageButton(permission, targetSdk)) {
518                                 // Uses "allow_always_radio_button" as id
519                                 byTextRes(R.string.allow_external_storage)
520                             } else {
521                                 By.res(ALLOW_RADIO_BUTTON)
522                             }
523                         PermissionState.DENIED ->
524                             if (!isLegacyApp && hasAskButton(permission)) {
525                                 By.res(ASK_RADIO_BUTTON)
526                             } else {
527                                 By.res(DENY_RADIO_BUTTON)
528                             }
529                         PermissionState.DENIED_WITH_PREJUDICE -> By.res(DENY_RADIO_BUTTON)
530                     }
531                 }
532             )
533             alreadyChecked = button.isChecked
534             if (!alreadyChecked) {
535                 button.click()
536             }
537             if (!alreadyChecked && isLegacyApp && wasGranted) {
538                 if (!isTv) {
539                     scrollToBottom()
540                 }
541 
542                 // Due to the limited real estate, Wear uses buttons with icons instead of text
543                 // for dialogs
544                 if (isWatch) {
545                     click(By.res(
546                         "com.android.permissioncontroller:id/wear_alertdialog_positive_button"))
547                 } else {
548                     val resources = context.createPackageContext(
549                         packageManager.permissionControllerPackageName, 0
550                     ).resources
551                     val confirmTextRes = resources.getIdentifier(
552                         "com.android.permissioncontroller:string/grant_dialog_button_deny_anyway",
553                         null, null
554                     )
555 
556                     val confirmText = resources.getString(confirmTextRes)
557                     click(byTextStartsWithCaseInsensitive(confirmText))
558                 }
559             }
560             pressBack()
561         }
562         pressBack()
563         pressBack()
564     }
565 
566     private fun getPermissionLabel(permission: String): String {
567         val labelResName = permissionToLabelResNameMap[permission]
568         assertNotNull("Unknown permission $permission", labelResName)
569         val labelRes = platformResources.getIdentifier(labelResName, null, null)
570         return platformResources.getString(labelRes)
571     }
572 
573     private fun hasAskButton(permission: String): Boolean =
574         when (permission) {
575             android.Manifest.permission.CAMERA,
576             android.Manifest.permission.RECORD_AUDIO,
577             android.Manifest.permission.ACCESS_FINE_LOCATION,
578             android.Manifest.permission.ACCESS_COARSE_LOCATION,
579             android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true
580             else -> false
581         }
582 
583     private fun showsForegroundOnlyButton(permission: String): Boolean =
584         when (permission) {
585             android.Manifest.permission.CAMERA,
586             android.Manifest.permission.RECORD_AUDIO -> true
587             else -> false
588         }
589 
590     private fun isMediaStorageButton(permission: String, targetSdk: Int): Boolean =
591             if (isTv || isWatch) {
592                 false
593             } else {
594                 when (permission) {
595                     android.Manifest.permission.READ_EXTERNAL_STORAGE,
596                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
597                     android.Manifest.permission.ACCESS_MEDIA_LOCATION ->
598                         // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set
599                         targetSdk >= Build.VERSION_CODES.P
600                     else -> false
601                 }
602             }
603 
604     private fun isAllStorageButton(permission: String, targetSdk: Int): Boolean =
605             if (isTv || isWatch) {
606                 false
607             } else {
608                 when (permission) {
609                     android.Manifest.permission.READ_EXTERNAL_STORAGE,
610                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
611                     android.Manifest.permission.ACCESS_MEDIA_LOCATION ->
612                         // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set
613                         targetSdk < Build.VERSION_CODES.P
614                     android.Manifest.permission.MANAGE_EXTERNAL_STORAGE -> true
615                     else -> false
616                 }
617             }
618 
619     private fun scrollToBottom() {
620         val scrollable = UiScrollable(UiSelector().scrollable(true)).apply {
621             swipeDeadZonePercentage = 0.25
622         }
623         waitForIdle()
624         if (scrollable.exists()) {
625             scrollable.flingToEnd(10)
626         }
627     }
628 
629     private fun byTextRes(textRes: Int): BySelector = By.text(context.getString(textRes))
630 
631     private fun byTextStartsWithCaseInsensitive(prefix: String): BySelector =
632         By.text(Pattern.compile("(?i)^${Pattern.quote(prefix)}.*$"))
633 
634     protected fun assertAppHasPermission(permissionName: String, expectPermission: Boolean) {
635         assertEquals(
636             if (expectPermission) {
637                 PackageManager.PERMISSION_GRANTED
638             } else {
639                 PackageManager.PERMISSION_DENIED
640             },
641             packageManager.checkPermission(permissionName, APP_PACKAGE_NAME)
642         )
643     }
644 
645     protected fun assertAppHasCalendarAccess(expectAccess: Boolean) {
646         val future = startActivityForFuture(
647             Intent().apply {
648                 component = ComponentName(
649                     APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.CheckCalendarAccessActivity"
650                 )
651             }
652         )
653         val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
654         assertEquals(Activity.RESULT_OK, result.resultCode)
655         assertTrue(result.resultData!!.hasExtra("$APP_PACKAGE_NAME.HAS_ACCESS"))
656         assertEquals(
657             expectAccess,
658             result.resultData!!.getBooleanExtra("$APP_PACKAGE_NAME.HAS_ACCESS", false)
659         )
660     }
661 }
662