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