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.permissionui.cts
18 
19 import android.Manifest
20 import android.app.Activity
21 import android.app.ActivityManager
22 import android.app.Instrumentation
23 import android.content.ComponentName
24 import android.content.Intent
25 import android.content.Intent.ACTION_REVIEW_APP_DATA_SHARING_UPDATES
26 import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
27 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
28 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
29 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
30 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_OTHER
31 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE
32 import android.content.pm.PackageInstaller.SessionParams
33 import android.content.pm.PackageManager
34 import android.net.Uri
35 import android.os.Build
36 import android.os.Process
37 import android.provider.DeviceConfig
38 import android.provider.Settings
39 import android.text.Spanned
40 import android.text.style.ClickableSpan
41 import android.view.View
42 import android.view.accessibility.AccessibilityNodeInfo
43 import androidx.test.uiautomator.By
44 import androidx.test.uiautomator.BySelector
45 import androidx.test.uiautomator.StaleObjectException
46 import androidx.test.uiautomator.UiObjectNotFoundException
47 import androidx.test.uiautomator.UiScrollable
48 import androidx.test.uiautomator.UiSelector
49 import androidx.test.uiautomator.Until
50 import com.android.compatibility.common.util.SystemUtil
51 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
52 import com.android.compatibility.common.util.SystemUtil.eventually
53 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
54 import com.android.modules.utils.build.SdkLevel
55 import java.util.concurrent.CompletableFuture
56 import java.util.concurrent.TimeUnit
57 import java.util.regex.Pattern
58 import org.junit.After
59 import org.junit.Assert
60 import org.junit.Assert.assertEquals
61 import org.junit.Assert.assertNotNull
62 import org.junit.Assert.assertTrue
63 import org.junit.Before
64 
65 abstract class BaseUsePermissionTest : BasePermissionTest() {
66     companion object {
67         const val APP_APK_NAME_31 = "CtsUsePermissionApp31.apk"
68         const val APP_APK_NAME_31_WITH_ASL = "CtsUsePermissionApp31WithAsl.apk"
69         const val APP_APK_NAME_LATEST = "CtsUsePermissionAppLatest.apk"
70 
71         const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk"
72         const val APP_APK_PATH_22_CALENDAR_ONLY =
73             "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk"
74         const val APP_APK_PATH_22_NONE = "$APK_DIRECTORY/CtsUsePermissionApp22None.apk"
75         const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk"
76         const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk"
77         const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk"
78         const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk"
79         const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk"
80         const val APP_APK_PATH_30 = "$APK_DIRECTORY/CtsUsePermissionApp30.apk"
81         const val APP_APK_PATH_31 = "$APK_DIRECTORY/$APP_APK_NAME_31"
82         const val APP_APK_PATH_32 = "$APK_DIRECTORY/CtsUsePermissionApp32.apk"
83 
84         const val APP_APK_PATH_30_WITH_BACKGROUND =
85             "$APK_DIRECTORY/CtsUsePermissionApp30WithBackground.apk"
86         const val APP_APK_PATH_30_WITH_BLUETOOTH =
87             "$APK_DIRECTORY/CtsUsePermissionApp30WithBluetooth.apk"
88         const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk"
89         const val APP_APK_PATH_LATEST_NONE = "$APK_DIRECTORY/CtsUsePermissionAppLatestNone.apk"
90         const val APP_APK_PATH_WITH_OVERLAY = "$APK_DIRECTORY/CtsUsePermissionAppWithOverlay.apk"
91         const val APP_APK_PATH_CREATE_NOTIFICATION_CHANNELS_31 =
92             "$APK_DIRECTORY/CtsCreateNotificationChannelsApp31.apk"
93         const val APP_APK_PATH_MEDIA_PERMISSION_33_WITH_STORAGE =
94             "$APK_DIRECTORY/CtsMediaPermissionApp33WithStorage.apk"
95         const val APP_APK_PATH_IMPLICIT_USER_SELECT_STORAGE =
96             "$APK_DIRECTORY/CtsUsePermissionAppImplicitUserSelectStorage.apk"
97         const val APP_APK_PATH_STORAGE_33 = "$APK_DIRECTORY/CtsUsePermissionAppStorage33.apk"
98         const val APP_APK_PATH_OTHER_APP = "$APK_DIRECTORY/CtsDifferentPkgNameApp.apk"
99         const val APP_APK_PATH_TWO_PERM_REQUESTS =
100             "$APK_DIRECTORY/CtsAppThatMakesTwoPermRequests.apk"
101         const val APP_PACKAGE_NAME = "android.permissionui.cts.usepermission"
102         const val OTHER_APP_PACKAGE_NAME = "android.permissionui.cts.usepermissionother"
103         const val TEST_INSTALLER_PACKAGE_NAME = "android.permissionui.cts"
104 
105         const val ALLOW_ALL_BUTTON =
106             "com.android.permissioncontroller:id/permission_allow_all_button"
107         const val SELECT_BUTTON =
108             "com.android.permissioncontroller:id/permission_allow_selected_button"
109         const val DONT_SELECT_MORE_BUTTON =
110             "com.android.permissioncontroller:id/permission_dont_allow_more_selected_button"
111         const val ALLOW_BUTTON = "com.android.permissioncontroller:id/permission_allow_button"
112         const val ALLOW_FOREGROUND_BUTTON =
113             "com.android.permissioncontroller:id/permission_allow_foreground_only_button"
114         const val DENY_BUTTON = "com.android.permissioncontroller:id/permission_deny_button"
115         const val DENY_AND_DONT_ASK_AGAIN_BUTTON =
116             "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button"
117         const val NO_UPGRADE_BUTTON =
118             "com.android.permissioncontroller:id/permission_no_upgrade_button"
119         const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON =
120             "com.android.permissioncontroller:" +
121                 "id/permission_no_upgrade_and_dont_ask_again_button"
122 
123         const val ALLOW_ALWAYS_RADIO_BUTTON =
124             "com.android.permissioncontroller:id/allow_always_radio_button"
125         const val ALLOW_RADIO_BUTTON = "com.android.permissioncontroller:id/allow_radio_button"
126         const val ALLOW_FOREGROUND_RADIO_BUTTON =
127             "com.android.permissioncontroller:id/allow_foreground_only_radio_button"
128         const val ASK_RADIO_BUTTON = "com.android.permissioncontroller:id/ask_radio_button"
129         const val DENY_RADIO_BUTTON = "com.android.permissioncontroller:id/deny_radio_button"
130         const val SELECT_RADIO_BUTTON = "com.android.permissioncontroller:id/select_radio_button"
131         const val EDIT_PHOTOS_BUTTON = "com.android.permissioncontroller:id/edit_selected_button"
132 
133         const val NOTIF_TEXT = "permgrouprequest_notifications"
134         const val ALLOW_BUTTON_TEXT = "grant_dialog_button_allow"
135         const val ALLOW_ALL_FILES_BUTTON_TEXT = "app_permission_button_allow_all_files"
136         const val ALLOW_FOREGROUND_BUTTON_TEXT = "grant_dialog_button_allow_foreground"
137         const val ALLOW_FOREGROUND_PREFERENCE_TEXT = "permission_access_only_foreground"
138         const val ASK_BUTTON_TEXT = "app_permission_button_ask"
139         const val ALLOW_ONE_TIME_BUTTON_TEXT = "grant_dialog_button_allow_one_time"
140         const val DENY_BUTTON_TEXT = "grant_dialog_button_deny"
141         const val DENY_ANYWAY_BUTTON_TEXT = "grant_dialog_button_deny_anyway"
142         const val DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT =
143             "grant_dialog_button_deny_and_dont_ask_again"
144         const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT = "grant_dialog_button_no_upgrade"
145         const val ECM_ALERT_DIALOG_OK_BUTTON_TEXT = "enhanced_confirmation_dialog_ok"
146         const val ALERT_DIALOG_MESSAGE = "android:id/message"
147         const val ALERT_DIALOG_OK_BUTTON = "android:id/button1"
148         const val APP_PERMISSION_RATIONALE_TITLE_TEXT = "app_location_permission_rationale_title"
149         const val APP_PERMISSION_RATIONALE_SUBTITLE_TEXT =
150             "app_location_permission_rationale_subtitle"
151         const val GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW =
152             "com.android.permissioncontroller:id/permission_rationale_container"
153         const val PERMISSION_RATIONALE_ACTIVITY_TITLE_VIEW =
154             "com.android.permissioncontroller:id/permission_rationale_title"
155         const val DATA_SHARING_SOURCE_TITLE_ID =
156             "com.android.permissioncontroller:id/data_sharing_source_title"
157         const val DATA_SHARING_SOURCE_MESSAGE_ID =
158             "com.android.permissioncontroller:id/data_sharing_source_message"
159         const val PURPOSE_TITLE_ID = "com.android.permissioncontroller:id/purpose_title"
160         const val PURPOSE_MESSAGE_ID = "com.android.permissioncontroller:id/purpose_message"
161         const val LEARN_MORE_TITLE_ID = "com.android.permissioncontroller:id/learn_more_title"
162         const val HELP_URL_ECM =
163             "com.android.permissioncontroller:id/help_url_action_disabled_by_restricted_settings"
164         const val LEARN_MORE_MESSAGE_ID = "com.android.permissioncontroller:id/learn_more_message"
165         const val DETAIL_MESSAGE_ID = "com.android.permissioncontroller:id/detail_message"
166         const val PERMISSION_RATIONALE_SETTINGS_SECTION =
167             "com.android.permissioncontroller:id/settings_section"
168         const val SETTINGS_TITLE_ID = "com.android.permissioncontroller:id/settings_title"
169         const val SETTINGS_MESSAGE_ID = "com.android.permissioncontroller:id/settings_message"
170 
171         const val REQUEST_LOCATION_MESSAGE = "permgrouprequest_location"
172 
173         const val DATA_SHARING_UPDATES = "Data sharing updates for location"
174         const val DATA_SHARING_UPDATES_SUBTITLE =
175             "These apps have changed the way they may share your location data. They may not" +
176                 " have shared it before, or may now share it for advertising or marketing" +
177                 " purposes."
178         const val DATA_SHARING_NO_UPDATES_MESSAGE = "No updates at this time"
179         const val UPDATES_IN_LAST_30_DAYS = "Updated within 30 days"
180         const val DATA_SHARING_UPDATES_FOOTER_MESSAGE =
181             "The developers of these apps provided info about their data sharing practices" +
182                 " to an app store. They may update it over time.\n\nData sharing" +
183                 " practices may vary based on your app version, use, region, and age."
184         const val LEARN_ABOUT_DATA_SHARING = "Learn about data sharing"
185         const val LOCATION_PERMISSION = "Location permission"
186         const val APP_PACKAGE_NAME_SUBSTRING = "android.permissionui"
187         const val NOW_SHARED_WITH_THIRD_PARTIES =
188             "Your location data is now shared with third " + "parties"
189         const val NOW_SHARED_WITH_THIRD_PARTIES_FOR_ADS =
190             "Your location data is now shared with " + "third parties for advertising or marketing"
191         const val PROPERTY_DATA_SHARING_UPDATE_PERIOD_MILLIS = "data_sharing_update_period_millis"
192         const val PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP =
193             "max_safety_labels_persisted_per_app"
194 
195         // The highest SDK for which the system will show a "low SDK" warning when launching the app
196         const val MAX_SDK_FOR_SDK_WARNING = 27
197         const val MIN_SDK_FOR_RUNTIME_PERMS = 23
198 
199         val TEST_INSTALLER_ACTIVITY_COMPONENT_NAME =
200             ComponentName(context, TestInstallerActivity::class.java)
201 
202         val MEDIA_PERMISSIONS: Set<String> =
203             mutableSetOf(
204                     Manifest.permission.ACCESS_MEDIA_LOCATION,
205                     Manifest.permission.READ_MEDIA_AUDIO,
206                     Manifest.permission.READ_MEDIA_IMAGES,
207                     Manifest.permission.READ_MEDIA_VIDEO,
208                 )
209                 .apply {
210                     if (SdkLevel.isAtLeastU()) {
211                         add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
212                     }
213                 }
214                 .toSet()
215 
216         val STORAGE_AND_MEDIA_PERMISSIONS =
217             MEDIA_PERMISSIONS.plus(Manifest.permission.READ_EXTERNAL_STORAGE)
218                 .plus(Manifest.permission.WRITE_EXTERNAL_STORAGE)
219 
220         @JvmStatic protected val PICKER_ENABLED_SETTING = "photo_picker_prompt_enabled"
221 
222         @JvmStatic
223         protected fun isPhotoPickerPermissionPromptEnabled(): Boolean {
224             return SdkLevel.isAtLeastU() &&
225                 !isTv &&
226                 !isAutomotive &&
227                 !isWatch &&
228                 callWithShellPermissionIdentity {
229                     DeviceConfig.getBoolean(
230                         DeviceConfig.NAMESPACE_PRIVACY,
231                         PICKER_ENABLED_SETTING,
232                         true
233                     )
234                 }
235         }
236     }
237 
238     enum class PermissionState {
239         ALLOWED,
240         DENIED,
241         DENIED_WITH_PREJUDICE
242     }
243 
244     private val platformResources = context.createPackageContext("android", 0).resources
245     private val permissionToLabelResNameMap =
246         mapOf(
247             // Contacts
248             android.Manifest.permission.READ_CONTACTS to "@android:string/permgrouplab_contacts",
249             android.Manifest.permission.WRITE_CONTACTS to "@android:string/permgrouplab_contacts",
250             // Calendar
251             android.Manifest.permission.READ_CALENDAR to "@android:string/permgrouplab_calendar",
252             android.Manifest.permission.WRITE_CALENDAR to "@android:string/permgrouplab_calendar",
253             // SMS
254             android.Manifest.permission_group.SMS to "@android:string/permgrouplab_sms",
255             android.Manifest.permission.SEND_SMS to "@android:string/permgrouplab_sms",
256             android.Manifest.permission.RECEIVE_SMS to "@android:string/permgrouplab_sms",
257             android.Manifest.permission.READ_SMS to "@android:string/permgrouplab_sms",
258             android.Manifest.permission.RECEIVE_WAP_PUSH to "@android:string/permgrouplab_sms",
259             android.Manifest.permission.RECEIVE_MMS to "@android:string/permgrouplab_sms",
260             "android.permission.READ_CELL_BROADCASTS" to "@android:string/permgrouplab_sms",
261             // Storage
262             android.Manifest.permission.READ_EXTERNAL_STORAGE to
263                 "@android:string/permgrouplab_storage",
264             android.Manifest.permission.WRITE_EXTERNAL_STORAGE to
265                 "@android:string/permgrouplab_storage",
266             // Location
267             android.Manifest.permission.ACCESS_FINE_LOCATION to
268                 "@android:string/permgrouplab_location",
269             android.Manifest.permission.ACCESS_COARSE_LOCATION to
270                 "@android:string/permgrouplab_location",
271             android.Manifest.permission.ACCESS_BACKGROUND_LOCATION to
272                 "@android:string/permgrouplab_location",
273             // Phone
274             android.Manifest.permission_group.PHONE to "@android:string/permgrouplab_phone",
275             android.Manifest.permission.READ_PHONE_STATE to "@android:string/permgrouplab_phone",
276             android.Manifest.permission.CALL_PHONE to "@android:string/permgrouplab_phone",
277             "android.permission.ACCESS_IMS_CALL_SERVICE" to "@android:string/permgrouplab_phone",
278             android.Manifest.permission.READ_CALL_LOG to "@android:string/permgrouplab_phone",
279             android.Manifest.permission.WRITE_CALL_LOG to "@android:string/permgrouplab_phone",
280             android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permgrouplab_phone",
281             android.Manifest.permission.USE_SIP to "@android:string/permgrouplab_phone",
282             android.Manifest.permission.PROCESS_OUTGOING_CALLS to
283                 "@android:string/permgrouplab_phone",
284             // Microphone
285             android.Manifest.permission.RECORD_AUDIO to "@android:string/permgrouplab_microphone",
286             // Camera
287             android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera",
288             // Body sensors
289             android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors",
290             android.Manifest.permission.BODY_SENSORS_BACKGROUND to
291                 "@android:string/permgrouplab_sensors",
292             // Bluetooth
293             android.Manifest.permission.BLUETOOTH_CONNECT to
294                 "@android:string/permgrouplab_nearby_devices",
295             android.Manifest.permission.BLUETOOTH_SCAN to
296                 "@android:string/permgrouplab_nearby_devices",
297             // Aural
298             android.Manifest.permission.READ_MEDIA_AUDIO to
299                 "@android:string/permgrouplab_readMediaAural",
300             // Visual
301             android.Manifest.permission.READ_MEDIA_IMAGES to
302                 "@android:string/permgrouplab_readMediaVisual",
303             android.Manifest.permission.READ_MEDIA_VIDEO to
304                 "@android:string/permgrouplab_readMediaVisual"
305         )
306 
307     @Before
308     @After
309     fun uninstallApp() {
310         uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false)
311     }
312 
313     override fun installPackage(
314         apkPath: String,
315         reinstall: Boolean,
316         grantRuntimePermissions: Boolean,
317         expectSuccess: Boolean,
318         installSource: String?
319     ) {
320         installPackage(
321             apkPath,
322             reinstall,
323             grantRuntimePermissions,
324             expectSuccess,
325             installSource,
326             false
327         )
328     }
329 
330     fun installPackage(
331         apkPath: String,
332         reinstall: Boolean = false,
333         grantRuntimePermissions: Boolean = false,
334         expectSuccess: Boolean = true,
335         installSource: String? = null,
336         skipClearLowSdkDialog: Boolean = false
337     ) {
338         super.installPackage(
339             apkPath,
340             reinstall,
341             grantRuntimePermissions,
342             expectSuccess,
343             installSource
344         )
345 
346         val targetSdk = getTargetSdk()
347         // If the targetSDK is high enough, the low sdk warning won't show. If the SDK is
348         // below runtime permissions, the dialog will be delayed by the permission review screen.
349         // If success is not expected, don't bother trying
350         if (
351             targetSdk > MAX_SDK_FOR_SDK_WARNING ||
352                 targetSdk < MIN_SDK_FOR_RUNTIME_PERMS ||
353                 !expectSuccess ||
354                 skipClearLowSdkDialog
355         ) {
356             return
357         }
358 
359         val finishOnCreateIntent =
360             Intent().apply {
361                 component =
362                     ComponentName(APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity")
363                 flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
364             }
365 
366         // Check if an activity resolves for the test app. If it doesn't, then our test app doesn't
367         // have the usual set of activities, and likely won't be opened, and thus, won't show the
368         // dialog
369         callWithShellPermissionIdentity {
370             context.packageManager.resolveActivity(finishOnCreateIntent, PackageManager.MATCH_ALL)
371         } ?: return
372 
373         // Start the test app, and expect the targetSDK warning dialog
374         context.startActivity(finishOnCreateIntent)
375         clearTargetSdkWarning()
376         // Kill the test app, so that the next time we launch, we don't see the app warning dialog
377         killTestApp()
378     }
379 
380     protected fun clearTargetSdkWarning(timeoutMillis: Long = TIMEOUT_MILLIS) {
381         if (SdkLevel.isAtLeastV()) {
382             // In V and above, the target SDK dialog can be disabled via system property
383             return
384         }
385 
386         val targetSdkWarningVisible =
387             uiDevice.wait(
388                 Until.hasObject(
389                     By.textStartsWith("This app was built for an older version of Android")
390                             .displayId(displayId)
391                 ),
392                 timeoutMillis
393             )
394         if (targetSdkWarningVisible) {
395             try {
396                 uiDevice.findObject(By.res("android:id/button1").displayId(displayId)).click()
397             } catch (e: StaleObjectException) {
398                 // Click sometimes fails with StaleObjectException (b/280430717).
399                 e.printStackTrace()
400             }
401         }
402     }
403 
404     protected fun killTestApp() {
405         pressBack()
406         pressBack()
407         runWithShellPermissionIdentity {
408             val am = context.getSystemService(ActivityManager::class.java)!!
409             am.forceStopPackage(APP_PACKAGE_NAME)
410         }
411         waitForIdle()
412     }
413 
414     protected fun clickPermissionReviewContinue() {
415         if (isAutomotive || isWatch) {
416             clickAndWaitForWindowTransition(
417                 By.text(getPermissionControllerString("review_button_continue"))
418                         .displayId(displayId),
419                 TIMEOUT_MILLIS * 2
420             )
421         } else {
422             clickAndWaitForWindowTransition(
423                 By.res("com.android.permissioncontroller:id/continue_button").displayId(displayId)
424             )
425         }
426     }
427 
428     protected fun clickPermissionReviewContinueAndClearSdkWarning() {
429         clickPermissionReviewContinue()
430         clearTargetSdkWarning()
431     }
432 
433     protected fun installPackageWithInstallSourceAndEmptyMetadata(apkName: String) {
434         installPackageViaSession(apkName, AppMetadata.createEmptyAppMetadata())
435     }
436 
437     protected fun installPackageWithInstallSourceAndMetadata(apkName: String) {
438         installPackageViaSession(apkName, AppMetadata.createDefaultAppMetadata())
439     }
440 
441     protected fun installPackageWithInstallSourceAndMetadataFromStore(apkName: String) {
442         installPackageViaSession(
443             apkName,
444             AppMetadata.createDefaultAppMetadata(),
445             PACKAGE_SOURCE_STORE
446         )
447     }
448 
449     protected fun installPackageWithInstallSourceAndMetadataFromLocalFile(apkName: String) {
450         installPackageViaSession(
451             apkName,
452             AppMetadata.createDefaultAppMetadata(),
453             PACKAGE_SOURCE_LOCAL_FILE
454         )
455     }
456 
457     protected fun installPackageWithInstallSourceAndMetadataFromDownloadedFile(apkName: String) {
458         installPackageViaSession(
459             apkName,
460             AppMetadata.createDefaultAppMetadata(),
461             PACKAGE_SOURCE_DOWNLOADED_FILE
462         )
463     }
464 
465     protected fun installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
466         apkName: String
467     ) {
468         installPackageViaSession(
469             apkName,
470             AppMetadata.createDefaultAppMetadata(),
471             PACKAGE_SOURCE_DOWNLOADED_FILE,
472             allowlistedRestrictedPermissions = SessionParams.RESTRICTED_PERMISSIONS_ALL
473         )
474     }
475 
476     protected fun installPackageWithInstallSourceAndMetadataFromOther(apkName: String) {
477         installPackageViaSession(
478             apkName,
479             AppMetadata.createDefaultAppMetadata(),
480             PACKAGE_SOURCE_OTHER
481         )
482     }
483 
484     protected fun installPackageWithInstallSourceAndNoMetadata(apkName: String) {
485         installPackageViaSession(apkName)
486     }
487 
488     protected fun installPackageWithInstallSourceAndNoMetadataFromStore(apkName: String) {
489         installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_STORE)
490     }
491 
492     protected fun installPackageWithInstallSourceAndNoMetadataFromLocalFile(apkName: String) {
493         installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_LOCAL_FILE)
494     }
495 
496     protected fun installPackageWithInstallSourceAndNoMetadataFromDownloadedFile(apkName: String) {
497         installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_DOWNLOADED_FILE)
498     }
499 
500     protected fun installPackageWithInstallSourceAndNoMetadataFromOther(apkName: String) {
501         installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_OTHER)
502     }
503 
504     protected fun installPackageWithInstallSourceAndInvalidMetadata(apkName: String) {
505         installPackageViaSession(apkName, AppMetadata.createInvalidAppMetadata())
506     }
507 
508     protected fun installPackageWithInstallSourceAndMetadataWithoutTopLevelVersion(
509         apkName: String
510     ) {
511         installPackageViaSession(
512             apkName,
513             AppMetadata.createInvalidAppMetadataWithoutTopLevelVersion()
514         )
515     }
516 
517     protected fun installPackageWithInstallSourceAndMetadataWithInvalidTopLevelVersion(
518         apkName: String
519     ) {
520         installPackageViaSession(
521             apkName,
522             AppMetadata.createInvalidAppMetadataWithInvalidTopLevelVersion()
523         )
524     }
525 
526     protected fun installPackageWithInstallSourceAndMetadataWithoutSafetyLabelVersion(
527         apkName: String
528     ) {
529         installPackageViaSession(
530             apkName,
531             AppMetadata.createInvalidAppMetadataWithoutSafetyLabelVersion()
532         )
533     }
534 
535     protected fun installPackageWithInstallSourceAndMetadataWithInvalidSafetyLabelVersion(
536         apkName: String
537     ) {
538         installPackageViaSession(
539             apkName,
540             AppMetadata.createInvalidAppMetadataWithInvalidSafetyLabelVersion()
541         )
542     }
543 
544     protected fun installPackageWithoutInstallSource(apkName: String) {
545         // TODO(b/257293222): Update/remove when hooking up PackageManager APIs
546         installPackage(apkName)
547     }
548 
549     protected fun assertPermissionRationaleActivityTitleIsVisible(expected: Boolean) {
550         findView(By.res(PERMISSION_RATIONALE_ACTIVITY_TITLE_VIEW).displayId(displayId),
551                 expected = expected)
552     }
553 
554     protected fun assertPermissionRationaleActivityDataSharingSourceSectionVisible(
555         expected: Boolean
556     ) {
557         findView(By.res(DATA_SHARING_SOURCE_TITLE_ID).displayId(displayId), expected = expected)
558         findView(By.res(DATA_SHARING_SOURCE_MESSAGE_ID).displayId(displayId), expected = expected)
559     }
560 
561     protected fun assertPermissionRationaleActivityPurposeSectionVisible(expected: Boolean) {
562         findView(By.res(PURPOSE_TITLE_ID).displayId(displayId), expected = expected)
563         findView(By.res(PURPOSE_MESSAGE_ID).displayId(displayId), expected = expected)
564     }
565 
566     protected fun assertPermissionRationaleActivityLearnMoreSectionVisible(expected: Boolean) {
567         findView(By.res(LEARN_MORE_TITLE_ID).displayId(displayId), expected = expected)
568         findView(By.res(LEARN_MORE_MESSAGE_ID).displayId(displayId), expected = expected)
569     }
570 
571     protected fun assertPermissionRationaleActivitySettingsSectionVisible(expected: Boolean) {
572         findView(By.res(PERMISSION_RATIONALE_SETTINGS_SECTION).displayId(displayId),
573                 expected = expected)
574         findView(By.res(SETTINGS_TITLE_ID).displayId(displayId), expected = expected)
575         findView(By.res(SETTINGS_MESSAGE_ID).displayId(displayId), expected = expected)
576     }
577 
578     protected fun assertPermissionRationaleDialogIsVisible(
579         expected: Boolean,
580         showSettingsSection: Boolean = true
581     ) {
582         assertPermissionRationaleActivityTitleIsVisible(expected)
583         assertPermissionRationaleActivityDataSharingSourceSectionVisible(expected)
584         assertPermissionRationaleActivityPurposeSectionVisible(expected)
585         assertPermissionRationaleActivityLearnMoreSectionVisible(expected)
586         if (expected) {
587             assertPermissionRationaleActivitySettingsSectionVisible(showSettingsSection)
588         }
589     }
590 
591     protected fun assertPermissionRationaleContainerOnGrantDialogIsVisible(expected: Boolean) {
592         findView(By.res(GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW).displayId(displayId),
593                 expected = expected)
594     }
595 
596     protected fun clickPermissionReviewCancel() {
597         if (isAutomotive || isWatch) {
598             clickAndWaitForWindowTransition(
599                 By.text(getPermissionControllerString("review_button_cancel")).displayId(displayId)
600             )
601         } else {
602             clickAndWaitForWindowTransition(
603                 By.res("com.android.permissioncontroller:id/cancel_button").displayId(displayId)
604             )
605         }
606     }
607 
608     protected fun approvePermissionReview() {
609         startAppActivityAndAssertResultCode(Activity.RESULT_OK) {
610             clickPermissionReviewContinueAndClearSdkWarning()
611         }
612     }
613 
614     protected fun cancelPermissionReview() {
615         startAppActivityAndAssertResultCode(Activity.RESULT_CANCELED) {
616             clickPermissionReviewCancel()
617         }
618     }
619 
620     protected fun assertAppDoesNotNeedPermissionReview() {
621         startAppActivityAndAssertResultCode(Activity.RESULT_OK) {}
622     }
623 
624     protected inline fun startAppActivityAndAssertResultCode(
625         expectedResultCode: Int,
626         block: () -> Unit
627     ) {
628         val future =
629             startActivityForFuture(
630                 Intent().apply {
631                     component =
632                         ComponentName(APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity")
633                 }
634             )
635         block()
636         assertEquals(
637             expectedResultCode,
638             future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).resultCode
639         )
640     }
641 
642     protected inline fun requestAppPermissionsForNoResult(
643         vararg permissions: String?,
644         crossinline block: () -> Unit
645     ) {
646         // Request the permissions
647         doAndWaitForWindowTransition {
648             context.startActivity(
649                 Intent().apply {
650                     component =
651                         ComponentName(
652                             APP_PACKAGE_NAME,
653                             "$APP_PACKAGE_NAME.RequestPermissionsActivity"
654                         )
655                     putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions)
656                     addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)
657                 }
658             )
659         }
660         // Perform the post-request action
661         block()
662     }
663 
664     protected inline fun requestAppPermissions(
665         vararg permissions: String?,
666         askTwice: Boolean = false,
667         waitForWindowTransition: Boolean = !isWatch,
668         crossinline block: () -> Unit
669     ): Instrumentation.ActivityResult {
670         // Request the permissions
671         lateinit var future: CompletableFuture<Instrumentation.ActivityResult>
672         doAndWaitForWindowTransition {
673             future =
674                 startActivityForFuture(
675                     Intent().apply {
676                         component =
677                             ComponentName(
678                                 APP_PACKAGE_NAME,
679                                 "$APP_PACKAGE_NAME.RequestPermissionsActivity"
680                             )
681                         putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions)
682                         putExtra("$APP_PACKAGE_NAME.ASK_TWICE", askTwice)
683                     }
684                 )
685         }
686 
687         // Notification permission prompt is shown first, so get it out of the way
688         clickNotificationPermissionRequestAllowButtonIfAvailable()
689         // Perform the post-request action
690         if (waitForWindowTransition) {
691             doAndWaitForWindowTransition { block() }
692         } else {
693             block()
694         }
695         return future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
696     }
697 
698     protected inline fun requestAppPermissionsAndAssertResult(
699         permissions: Array<out String?>,
700         permissionAndExpectedGrantResults: Array<out Pair<String?, Boolean>>,
701         askTwice: Boolean = false,
702         waitForWindowTransition: Boolean = !isWatch,
703         crossinline block: () -> Unit
704     ) {
705         var shouldWaitForWindowTransition = waitForWindowTransition
706         // Do not wait for windowTransition after action is performed on auto, when permissions
707         // are being denied. The click deny function explicitly waits for window to transition
708         if (isAutomotive) {
709             var somePermissionsTrue = false
710             // http://go/nl-kt-best-practices#for-loop-vs-foreach
711             for (it in permissionAndExpectedGrantResults) {
712                 somePermissionsTrue = somePermissionsTrue || it.second
713             }
714             // When all permissions being requested are to be denied
715             // do not wait for windowTransition
716             if (!somePermissionsTrue) {
717                 shouldWaitForWindowTransition = false
718             }
719         }
720         val result =
721             requestAppPermissions(
722                 *permissions,
723                 askTwice = askTwice,
724                 waitForWindowTransition = shouldWaitForWindowTransition,
725                 block = block
726             )
727         assertEquals(
728             "Permission request result had unexpected resultCode:",
729             Activity.RESULT_OK,
730             result.resultCode
731         )
732 
733         val responseSize: Int =
734             result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!.size
735         assertEquals(
736             "Permission request result had unexpected number of grant results:",
737             responseSize,
738             result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.size
739         )
740 
741         // Note that the behavior around requesting `null` permissions changed in the platform
742         // in Android U. Currently, null permissions are ignored and left out of the result set.
743         assertTrue(
744             "Permission request result had fewer permissions than request",
745             permissions.size >= responseSize
746         )
747         assertEquals(
748             "Permission request result had unexpected grant results:",
749             permissionAndExpectedGrantResults.filter { it.first != null }.toList(),
750             result.resultData!!
751                 .getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!
752                 .filterNotNull()
753                 .zip(
754                     result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.map {
755                         it == PackageManager.PERMISSION_GRANTED
756                     }
757                 )
758         )
759 
760         permissionAndExpectedGrantResults.forEach {
761             it.first?.let { permission -> assertAppHasPermission(permission, it.second) }
762         }
763     }
764 
765     protected inline fun requestAppPermissionsAndAssertResult(
766         vararg permissionAndExpectedGrantResults: Pair<String?, Boolean>,
767         askTwice: Boolean = false,
768         waitForWindowTransition: Boolean = !isWatch,
769         crossinline block: () -> Unit
770     ) {
771         requestAppPermissionsAndAssertResult(
772             permissionAndExpectedGrantResults.map { it.first }.toTypedArray(),
773             permissionAndExpectedGrantResults,
774             askTwice,
775             waitForWindowTransition,
776             block
777         )
778     }
779 
780     // Perform the requested action, then wait both for the action to complete, and for at least
781     // one window transition to occur since the moment the action begins executing.
782     protected inline fun doAndWaitForWindowTransition(crossinline block: () -> Unit) {
783         val timeoutOccurred =
784             !uiDevice.performActionAndWait(
785                 { block() },
786                 Until.newWindow(),
787                 NEW_WINDOW_TIMEOUT_MILLIS
788             )
789 
790         if (timeoutOccurred) {
791             throw RuntimeException("Timed out waiting for window transition.")
792         }
793     }
794 
795     protected fun findPermissionRequestAllowButton(timeoutMillis: Long = 20000) {
796         if (isAutomotive || isWatch) {
797             waitFindObject(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT))
798                     .displayId(displayId), timeoutMillis)
799         } else {
800             waitFindObject(By.res(ALLOW_BUTTON).displayId(displayId), timeoutMillis)
801         }
802     }
803 
804     protected fun clickPermissionRequestAllowButton(timeoutMillis: Long = 20000) {
805         if (isAutomotive || isWatch) {
806             click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)).displayId(displayId),
807                     timeoutMillis)
808         } else {
809             click(By.res(ALLOW_BUTTON).displayId(displayId), timeoutMillis)
810         }
811     }
812 
813     protected fun clickPermissionRequestAllowAllButton(timeoutMillis: Long = 20000) {
814         click(By.res(ALLOW_ALL_BUTTON).displayId(displayId), timeoutMillis)
815     }
816 
817     /**
818      * Only for use in tests that are not testing the notification permission popup, on T devices
819      */
820     protected fun clickNotificationPermissionRequestAllowButtonIfAvailable() {
821         if (SdkLevel.isAtLeastT() && getTargetSdk() < Build.VERSION_CODES.TIRAMISU) {
822             val notificationPermissionRequestVisible =
823                 uiDevice.wait(
824                     Until.hasObject(
825                         By.text(getPermissionControllerString(NOTIF_TEXT, APP_PACKAGE_NAME))
826                                 .displayId(displayId)
827                     ),
828                     1000
829                 )
830             if (notificationPermissionRequestVisible) {
831                 if (isAutomotive) {
832                     click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT))
833                             .displayId(displayId))
834                 } else {
835                     click(By.res(ALLOW_BUTTON).displayId(displayId))
836                 }
837             }
838         }
839     }
840 
841     protected fun clickPermissionRequestSettingsLinkAndAllowAlways() {
842         clickPermissionRequestSettingsLink()
843         eventually({ clickAllowAlwaysInSettings() }, TIMEOUT_MILLIS * 2)
844         pressBack()
845     }
846 
847     protected fun clickAllowAlwaysInSettings() {
848         if (isAutomotive || isTv || isWatch) {
849             click(By.text(getPermissionControllerString("app_permission_button_allow_always"))
850                     .displayId(displayId))
851         } else {
852             click(By.res("com.android.permissioncontroller:id/allow_always_radio_button")
853                     .displayId(displayId))
854         }
855     }
856 
857     protected fun clickAllowForegroundInSettings() {
858         click(By.res(ALLOW_FOREGROUND_RADIO_BUTTON).displayId(displayId))
859     }
860 
861     protected fun clicksDenyInSettings() {
862         if (isAutomotive || isWatch) {
863             click(By.text(getPermissionControllerString("app_permission_button_deny"))
864                     .displayId(displayId))
865         } else {
866             click(By.res("com.android.permissioncontroller:id/deny_radio_button")
867                     .displayId(displayId))
868         }
869     }
870 
871     protected fun findPermissionRequestAllowForegroundButton(timeoutMillis: Long = 20000) {
872         if (isAutomotive || isWatch) {
873             waitFindObject(
874                 By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
875                         .displayId(displayId),
876                 timeoutMillis
877             )
878         } else {
879             waitFindObject(By.res(ALLOW_FOREGROUND_BUTTON).displayId(displayId), timeoutMillis)
880         }
881     }
882 
883     protected fun clickPermissionRequestAllowForegroundButton(timeoutMillis: Long = 20_000) {
884         if (isAutomotive || isWatch) {
885             click(
886                 By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT))
887                         .displayId(displayId),
888                 timeoutMillis
889             )
890         } else {
891             click(By.res(ALLOW_FOREGROUND_BUTTON).displayId(displayId), timeoutMillis)
892         }
893     }
894 
895     protected fun clickPermissionRequestDenyButton() {
896         if (isAutomotive) {
897             scrollToBottom()
898             clickAndWaitForWindowTransition(
899                 By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId)
900             )
901         } else if (isWatch || isTv) {
902             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId))
903         } else {
904             click(By.res(DENY_BUTTON).displayId(displayId))
905         }
906     }
907 
908     protected fun clickPermissionRequestSettingsLinkAndDeny() {
909         clickPermissionRequestSettingsLink()
910         eventually({ clicksDenyInSettings() }, TIMEOUT_MILLIS * 2)
911         pressBack()
912     }
913 
914     protected fun clickPermissionRequestSettingsLink() {
915         eventually {
916             if (isWatch) {
917                 clickPermissionRequestSettingsLinkForWear()
918                 return@eventually
919             }
920             // UiObject2 doesn't expose CharSequence.
921             val node =
922                 if (isAutomotive) {
923                     // Should match "Allow in settings." (location) and "go to settings." (body
924                     // sensors)
925                     uiAutomation.rootInActiveWindow
926                         .findAccessibilityNodeInfosByText(" settings.")[0]
927                 } else {
928                     uiAutomation.rootInActiveWindow
929                         .findAccessibilityNodeInfosByViewId(DETAIL_MESSAGE_ID)[0]
930                 }
931             if (!node.isVisibleToUser) {
932                 scrollToBottom()
933             }
934             assertTrue(node.isVisibleToUser)
935 
936             val text = node.text as Spanned
937             val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0]
938             // We could pass in null here in Java, but we need an instance in Kotlin.
939             doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) }
940         }
941     }
942 
943     private fun clickPermissionRequestSettingsLinkForWear() {
944         // Find detail message.
945         val text = waitFindObject(By.textContains(" settings.").displayId(displayId))
946 
947         // Move the view to the top of the screen.
948         var visibleBounds = text.getVisibleBounds()
949         val centerX = (visibleBounds.left + visibleBounds.right) / 2
950         uiDevice.drag(centerX, visibleBounds.top, centerX, 0, 10)
951 
952         // Click the deep link.
953         // Not sure where the clickable text is. So try different point in the last line
954         // of the 5 line text.
955         val bounds = text.getVisibleBounds()
956         val xdelta = 0.2 * bounds.width()
957         val y = bounds.bottom - (0.05 * bounds.height())
958         var clickedOnLink: Boolean = false
959         for (i in 1..4) {
960             val x = bounds.left + (i * xdelta)
961             uiDevice.click(x.toInt(), y.toInt())
962             waitForIdleLong()
963             val nextScreenNode: AccessibilityNodeInfo? =
964                 findAccessibilityNodeInfosByTextForSurfaceView(
965                     uiAutomation.rootInActiveWindow,
966                     "All the time"
967                 )
968             if (nextScreenNode != null) {
969                 clickedOnLink = true
970                 break
971             }
972         }
973         assertTrue("Could not click on the settings link correctly", clickedOnLink)
974     }
975 
976     protected fun clickPermissionRequestDenyAndDontAskAgainButton() {
977         if (isAutomotive) {
978             scrollToBottom()
979             clickAndWaitForWindowTransition(
980                 By.text(getPermissionControllerString(DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT))
981                         .displayId(displayId)
982             )
983         } else if (isWatch) {
984             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId))
985         } else {
986             click(By.res(DENY_AND_DONT_ASK_AGAIN_BUTTON).displayId(displayId))
987         }
988     }
989 
990     // Only used in TV and Watch form factors
991     protected fun clickPermissionRequestDontAskAgainButton() {
992         if (isWatch) {
993             click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId))
994         } else {
995             click(
996                 By.res("com.android.permissioncontroller:id/permission_deny_dont_ask_again_button")
997                         .displayId(displayId)
998             )
999         }
1000     }
1001 
1002     protected fun clickPermissionRequestNoUpgradeAndDontAskAgainButton() {
1003         if (isAutomotive || isWatch) {
1004             click(By.text(getPermissionControllerString(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT))
1005                     .displayId(displayId))
1006         } else {
1007             click(By.res(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON).displayId(displayId))
1008         }
1009     }
1010 
1011     protected fun clickPermissionRationaleContentInAppPermission() {
1012         clickAndWaitForWindowTransition(
1013             By.text(getPermissionControllerString(APP_PERMISSION_RATIONALE_SUBTITLE_TEXT))
1014                     .displayId(displayId)
1015         )
1016     }
1017 
1018     protected fun clickPermissionRationaleViewInGrantDialog() {
1019         clickAndWaitForWindowTransition(
1020                 By.res(GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW).displayId(displayId))
1021     }
1022 
1023     protected fun grantAppPermissionsByUi(vararg permissions: String) {
1024         setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false)
1025     }
1026 
1027     protected fun grantRuntimePermissions(vararg permissions: String) {
1028         for (permission in permissions) {
1029             uiAutomation.grantRuntimePermission(APP_PACKAGE_NAME, permission)
1030         }
1031     }
1032 
1033     protected fun revokeAppPermissionsByUi(
1034         vararg permissions: String,
1035         isLegacyApp: Boolean = false
1036     ) {
1037         setAppPermissionState(
1038             *permissions,
1039             state = PermissionState.DENIED,
1040             isLegacyApp = isLegacyApp
1041         )
1042     }
1043 
1044     private fun navigateToAppPermissionSettings() {
1045         if (isTv) {
1046             clearTargetSdkWarning(1000L)
1047             pressHome()
1048         } else {
1049             pressBack()
1050             pressBack()
1051             pressBack()
1052         }
1053 
1054         // Try multiple times as the AppInfo page might have read stale data
1055         eventually(
1056             {
1057                 try {
1058                     // Open the app details settings
1059                     doAndWaitForWindowTransition {
1060                         context.startActivity(
1061                             Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
1062                                 data = Uri.fromParts("package", APP_PACKAGE_NAME, null)
1063                                 addCategory(Intent.CATEGORY_DEFAULT)
1064                                 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1065                                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
1066                             }
1067                         )
1068                     }
1069                     if (isTv) {
1070                         pressDPadDown()
1071                         pressDPadDown()
1072                         pressDPadDown()
1073                         pressDPadDown()
1074                     }
1075                     // Open the permissions UI
1076                     clickAndWaitForWindowTransition(byTextRes(R.string.permissions).enabled(true))
1077                 } catch (e: Exception) {
1078                     pressBack()
1079                     throw e
1080                 }
1081             },
1082             TIMEOUT_MILLIS
1083         )
1084     }
1085 
1086     private fun getTargetSdk(packageName: String = APP_PACKAGE_NAME): Int {
1087         return callWithShellPermissionIdentity {
1088             try {
1089                 context.packageManager.getApplicationInfo(packageName, 0).targetSdkVersion
1090             } catch (e: PackageManager.NameNotFoundException) {
1091                 -1
1092             }
1093         }
1094     }
1095 
1096     protected fun navigateToIndividualPermissionSetting(
1097         permission: String,
1098         manuallyNavigate: Boolean = false
1099     ) {
1100         val useLegacyNavigation = isWatch || isAutomotive || manuallyNavigate
1101         if (useLegacyNavigation) {
1102             navigateToAppPermissionSettings()
1103             val permissionLabel = getPermissionLabel(permission)
1104             if (isWatch) {
1105                 clickAndWaitForWindowTransition(By.text(permissionLabel).displayId(displayId),
1106                         40_000)
1107             } else {
1108                 clickPermissionControllerUi(By.text(permissionLabel).displayId(displayId))
1109             }
1110             return
1111         }
1112         doAndWaitForWindowTransition {
1113             runWithShellPermissionIdentity {
1114                 context.startActivity(
1115                     Intent(Intent.ACTION_MANAGE_APP_PERMISSION).apply {
1116                         putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME)
1117                         putExtra(Intent.EXTRA_PERMISSION_NAME, permission)
1118                         putExtra(Intent.EXTRA_USER, Process.myUserHandle())
1119                         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1120                         addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
1121                     }
1122                 )
1123             }
1124         }
1125     }
1126 
1127     @Suppress("DEPRECATION")
1128     protected fun startManageAppPermissionsActivity() {
1129         doAndWaitForWindowTransition {
1130             runWithShellPermissionIdentity {
1131                 context.startActivity(
1132                     Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply {
1133                         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1134                         addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
1135                         putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME)
1136                     }
1137                 )
1138             }
1139         }
1140     }
1141 
1142     /** Starts activity with intent [ACTION_REVIEW_APP_DATA_SHARING_UPDATES]. */
1143     fun startAppDataSharingUpdatesActivity() {
1144         doAndWaitForWindowTransition {
1145             runWithShellPermissionIdentity {
1146                 context.startActivity(
1147                     Intent(ACTION_REVIEW_APP_DATA_SHARING_UPDATES).apply {
1148                         addFlags(FLAG_ACTIVITY_NEW_TASK)
1149                     }
1150                 )
1151             }
1152         }
1153     }
1154 
1155     private fun setAppPermissionState(
1156         vararg permissions: String,
1157         state: PermissionState,
1158         isLegacyApp: Boolean,
1159         manuallyNavigate: Boolean = false,
1160     ) {
1161         val useLegacyNavigation = isWatch || isAutomotive || manuallyNavigate
1162         if (useLegacyNavigation) {
1163             navigateToAppPermissionSettings()
1164         }
1165 
1166         val navigatedGroupLabels = mutableSetOf<String>()
1167         for (permission in permissions) {
1168             // Find the permission screen
1169             val permissionLabel = getPermissionLabel(permission)
1170             if (navigatedGroupLabels.contains(getPermissionLabel(permission))) {
1171                 continue
1172             }
1173             navigatedGroupLabels.add(permissionLabel)
1174             if (useLegacyNavigation) {
1175                 if (isWatch) {
1176                     click(By.text(permissionLabel).displayId(displayId), 40_000)
1177                 } else if (isAutomotive) {
1178                     clickPermissionControllerUi(permissionLabel)
1179                 } else {
1180                     clickPermissionControllerUi(By.text(permissionLabel).displayId(displayId))
1181                 }
1182             } else {
1183                 doAndWaitForWindowTransition {
1184                     runWithShellPermissionIdentity {
1185                         context.startActivity(
1186                             Intent(Intent.ACTION_MANAGE_APP_PERMISSION).apply {
1187                                 putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME)
1188                                 putExtra(Intent.EXTRA_PERMISSION_NAME, permission)
1189                                 putExtra(Intent.EXTRA_USER, Process.myUserHandle())
1190                                 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1191                                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
1192                             }
1193                         )
1194                     }
1195                 }
1196             }
1197 
1198             val wasGranted =
1199                 if (isAutomotive) {
1200                     // Automotive doesn't support one time permissions, and thus
1201                     // won't show an "Ask every time" message
1202                     !waitFindObject(
1203                             By.text(getPermissionControllerString("app_permission_button_deny"))
1204                                     .displayId(displayId)
1205                         )
1206                         .isChecked
1207                 } else if (isTv || isWatch) {
1208                     !(waitFindObject(By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
1209                             .displayId(displayId))
1210                         .isChecked ||
1211                         (!isLegacyApp &&
1212                             hasAskButton(permission) &&
1213                             waitFindObject(By.text(getPermissionControllerString(ASK_BUTTON_TEXT))
1214                                     .displayId(displayId))
1215                                 .isChecked))
1216                 } else {
1217                     !(waitFindObject(By.res(DENY_RADIO_BUTTON).displayId(displayId)).isChecked ||
1218                         (!isLegacyApp &&
1219                             hasAskButton(permission) &&
1220                             waitFindObject(By.res(ASK_RADIO_BUTTON).displayId(displayId))
1221                                     .isChecked))
1222                 }
1223             var alreadyChecked = false
1224             val button =
1225                 waitFindObject(
1226                     if (isAutomotive) {
1227                         // Automotive doesn't support one time permissions, and thus
1228                         // won't show an "Ask every time" message
1229                         when (state) {
1230                             PermissionState.ALLOWED ->
1231                                 if (showsForegroundOnlyButton(permission)) {
1232                                     By.text(
1233                                         getPermissionControllerString(
1234                                             "app_permission_button_allow_foreground"
1235                                         )
1236                                     ).displayId(displayId)
1237                                 } else {
1238                                     By.text(
1239                                         getPermissionControllerString("app_permission_button_allow")
1240                                     ).displayId(displayId)
1241                                 }
1242                             PermissionState.DENIED ->
1243                                 By.text(getPermissionControllerString("app_permission_button_deny"))
1244                                         .displayId(displayId)
1245                             PermissionState.DENIED_WITH_PREJUDICE ->
1246                                 By.text(getPermissionControllerString("app_permission_button_deny"))
1247                                         .displayId(displayId)
1248                         }
1249                     } else if (isTv || isWatch) {
1250                         when (state) {
1251                             PermissionState.ALLOWED ->
1252                                 if (showsForegroundOnlyButton(permission)) {
1253                                     By.text(
1254                                         getPermissionControllerString(
1255                                             ALLOW_FOREGROUND_PREFERENCE_TEXT
1256                                         )
1257                                     ).displayId(displayId)
1258                                 } else {
1259                                     byAnyText(
1260                                         getPermissionControllerResString(ALLOW_BUTTON_TEXT),
1261                                         getPermissionControllerResString(
1262                                             ALLOW_ALL_FILES_BUTTON_TEXT
1263                                         )
1264                                     )
1265                                 }
1266                             PermissionState.DENIED ->
1267                                 if (!isLegacyApp && hasAskButton(permission)) {
1268                                     By.text(getPermissionControllerString(ASK_BUTTON_TEXT))
1269                                             .displayId(displayId)
1270                                 } else {
1271                                     By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
1272                                             .displayId(displayId)
1273                                 }
1274                             PermissionState.DENIED_WITH_PREJUDICE ->
1275                                 By.text(getPermissionControllerString(DENY_BUTTON_TEXT))
1276                                         .displayId(displayId)
1277                         }
1278                     } else {
1279                         when (state) {
1280                             PermissionState.ALLOWED ->
1281                                 if (showsForegroundOnlyButton(permission)) {
1282                                     By.res(ALLOW_FOREGROUND_RADIO_BUTTON).displayId(displayId)
1283                                 } else if (showsAlwaysButton(permission)) {
1284                                     By.res(ALLOW_ALWAYS_RADIO_BUTTON).displayId(displayId)
1285                                 } else {
1286                                     By.res(ALLOW_RADIO_BUTTON).displayId(displayId)
1287                                 }
1288                             PermissionState.DENIED ->
1289                                 if (!isLegacyApp && hasAskButton(permission)) {
1290                                     By.res(ASK_RADIO_BUTTON).displayId(displayId)
1291                                 } else {
1292                                     By.res(DENY_RADIO_BUTTON).displayId(displayId)
1293                                 }
1294                             PermissionState.DENIED_WITH_PREJUDICE -> By.res(DENY_RADIO_BUTTON)
1295                                     .displayId(displayId)
1296                         }
1297                     }
1298                 )
1299             alreadyChecked = button.isChecked
1300             if (!alreadyChecked) {
1301                 button.click()
1302             }
1303 
1304             val shouldShowStorageWarning =
1305                 SdkLevel.isAtLeastT() &&
1306                     getTargetSdk() <= Build.VERSION_CODES.S_V2 &&
1307                     permission in MEDIA_PERMISSIONS
1308             if (shouldShowStorageWarning) {
1309                 if (isWatch) {
1310                     click(
1311                         By.desc(
1312                             getPermissionControllerString("media_confirm_dialog_positive_button")
1313                         ).displayId(displayId)
1314                     )
1315                 } else {
1316                     click(By.res(ALERT_DIALOG_OK_BUTTON).displayId(displayId))
1317                 }
1318             } else if (!alreadyChecked && isLegacyApp && wasGranted) {
1319                 if (!isTv) {
1320                     // Wait for alert dialog to popup, then scroll to the bottom of it
1321                     if (isWatch) {
1322                         waitFindObject(
1323                             By.text(getPermissionControllerString("old_sdk_deny_warning"))
1324                                     .displayId(displayId)
1325                         )
1326                     } else {
1327                         waitFindObject(By.res(ALERT_DIALOG_MESSAGE).displayId(displayId))
1328                     }
1329                     scrollToBottom()
1330                 }
1331 
1332                 // Due to the limited real estate, Wear uses buttons with icons instead of text
1333                 // for dialogs
1334                 if (isWatch) {
1335                     click(By.desc(getPermissionControllerString("ok")).displayId(displayId))
1336                 } else {
1337                     val resources =
1338                         context
1339                             .createPackageContext(packageManager.permissionControllerPackageName, 0)
1340                             .resources
1341                     val confirmTextRes =
1342                         resources.getIdentifier(
1343                             "com.android.permissioncontroller:string/grant_dialog_button_deny_anyway",
1344                             null,
1345                             null
1346                         )
1347 
1348                     val confirmText = resources.getString(confirmTextRes)
1349                     click(byTextStartsWithCaseInsensitive(confirmText))
1350                 }
1351             }
1352             pressBack()
1353         }
1354         pressBack()
1355         pressBack()
1356     }
1357 
1358     private fun getPermissionLabel(permission: String): String {
1359         val labelResName = permissionToLabelResNameMap[permission]
1360         assertNotNull("Unknown permission $permission", labelResName)
1361         val labelRes = platformResources.getIdentifier(labelResName, null, null)
1362         return platformResources.getString(labelRes)
1363     }
1364 
1365     private fun hasAskButton(permission: String): Boolean =
1366         when (permission) {
1367             android.Manifest.permission.CAMERA,
1368             android.Manifest.permission.RECORD_AUDIO,
1369             android.Manifest.permission.ACCESS_FINE_LOCATION,
1370             android.Manifest.permission.ACCESS_COARSE_LOCATION,
1371             android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true
1372             else -> false
1373         }
1374 
1375     private fun showsAllowPhotosButton(permission: String): Boolean {
1376         if (!isPhotoPickerPermissionPromptEnabled()) {
1377             return false
1378         }
1379         return when (permission) {
1380             Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
1381             Manifest.permission.READ_MEDIA_IMAGES,
1382             Manifest.permission.READ_MEDIA_VIDEO -> true
1383             else -> false
1384         }
1385     }
1386 
1387     private fun showsForegroundOnlyButton(permission: String): Boolean =
1388         when (permission) {
1389             android.Manifest.permission.CAMERA,
1390             android.Manifest.permission.RECORD_AUDIO -> true
1391             else -> false
1392         }
1393 
1394     private fun showsAlwaysButton(permission: String): Boolean =
1395         when (permission) {
1396             android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true
1397             else -> false
1398         }
1399 
1400     private fun scrollToBottom() {
1401         val scrollable =
1402             UiScrollable(UiSelector().scrollable(true)).apply {
1403                 if (isWatch) {
1404                     swipeDeadZonePercentage = 0.1
1405                 } else {
1406                     swipeDeadZonePercentage = 0.25
1407                 }
1408             }
1409         waitForIdle()
1410         if (scrollable.exists()) {
1411             try {
1412                 scrollable.flingToEnd(10)
1413             } catch (e: UiObjectNotFoundException) {
1414                 // flingToEnd() sometimes still fails despite waitForIdle() and the exists() check
1415                 // (b/246984354).
1416                 e.printStackTrace()
1417             }
1418         }
1419     }
1420 
1421     protected fun findAccessibilityNodeInfosByTextForSurfaceView(
1422         node: AccessibilityNodeInfo,
1423         text: String
1424     ): AccessibilityNodeInfo? {
1425         if (node.text != null && node.text.contains(text)) return node
1426         for (i in 0 until node.childCount) {
1427             val child = node.getChild(i)
1428             if (child != null) {
1429                 return findAccessibilityNodeInfosByTextForSurfaceView(child, text) ?: continue
1430             }
1431         }
1432         return null
1433     }
1434 
1435     private fun byTextRes(textRes: Int): BySelector =
1436             By.text(context.getString(textRes)).displayId(displayId)
1437 
1438     private fun byTextStartsWithCaseInsensitive(prefix: String): BySelector =
1439         By.text(Pattern.compile("(?i)^${Pattern.quote(prefix)}.*$")).displayId(displayId)
1440 
1441     protected fun assertAppHasPermission(permissionName: String, expectPermission: Boolean) {
1442         val checkPermissionResult = packageManager.checkPermission(permissionName, APP_PACKAGE_NAME)
1443         assertTrue(
1444             "Invalid permission check result: $checkPermissionResult",
1445             checkPermissionResult == PackageManager.PERMISSION_GRANTED ||
1446                 checkPermissionResult == PackageManager.PERMISSION_DENIED
1447         )
1448         if (!expectPermission && checkPermissionResult == PackageManager.PERMISSION_GRANTED) {
1449             Assert.fail(
1450                 "Unexpected permission check result for $permissionName: " +
1451                     "expected -1 (PERMISSION_DENIED) but was 0 (PERMISSION_GRANTED)"
1452             )
1453         }
1454         if (expectPermission && checkPermissionResult == PackageManager.PERMISSION_DENIED) {
1455             Assert.fail(
1456                 "Unexpected permission check result for $permissionName: " +
1457                     "expected 0 (PERMISSION_GRANTED) but was -1 (PERMISSION_DENIED)"
1458             )
1459         }
1460     }
1461 
1462     protected fun assertAppHasCalendarAccess(expectAccess: Boolean) {
1463         val future =
1464             startActivityForFuture(
1465                 Intent().apply {
1466                     component =
1467                         ComponentName(
1468                             APP_PACKAGE_NAME,
1469                             "$APP_PACKAGE_NAME.CheckCalendarAccessActivity"
1470                         )
1471                 }
1472             )
1473         clickNotificationPermissionRequestAllowButtonIfAvailable()
1474         val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
1475         assertEquals(Activity.RESULT_OK, result.resultCode)
1476         assertTrue(result.resultData!!.hasExtra("$APP_PACKAGE_NAME.HAS_ACCESS"))
1477         assertEquals(
1478             expectAccess,
1479             result.resultData!!.getBooleanExtra("$APP_PACKAGE_NAME.HAS_ACCESS", false)
1480         )
1481     }
1482 
1483     protected fun assertPermissionFlags(permName: String, vararg flags: Pair<Int, Boolean>) {
1484         val user = Process.myUserHandle()
1485         SystemUtil.runWithShellPermissionIdentity {
1486             val currFlags = packageManager.getPermissionFlags(permName, APP_PACKAGE_NAME, user)
1487             for ((flag, set) in flags) {
1488                 assertEquals("flag $flag: ", set, currFlags and flag != 0)
1489             }
1490         }
1491     }
1492 
1493     protected fun clickECMAlertDialogOKButton(waitForWindowTransition: Boolean = !isWatch) {
1494         var action = { click(By.res(ALERT_DIALOG_OK_BUTTON).displayId(displayId), TIMEOUT_MILLIS) }
1495         if (isWatch) {
1496             val okButtonText =
1497                 getPermissionControllerResString(ECM_ALERT_DIALOG_OK_BUTTON_TEXT) ?: "OK"
1498             action = { click(By.text(okButtonText).displayId(displayId), TIMEOUT_MILLIS) }
1499         }
1500 
1501         if (waitForWindowTransition) {
1502             doAndWaitForWindowTransition { action() }
1503         } else {
1504             action()
1505         }
1506     }
1507 }
1508