1 /*
2  * 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 com.android.cts.usepermission;
18 
19 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
20 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
21 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
22 
23 import static junit.framework.Assert.assertEquals;
24 
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.fail;
27 
28 import android.Manifest;
29 import android.app.Activity;
30 import android.app.Instrumentation;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageManager;
34 import android.content.pm.ResolveInfo;
35 import android.content.res.Resources;
36 import android.icu.text.CaseMap;
37 import android.net.Uri;
38 import android.os.Bundle;
39 import android.os.SystemClock;
40 import android.os.UserHandle;
41 import android.provider.Settings;
42 import android.support.test.uiautomator.By;
43 import android.support.test.uiautomator.BySelector;
44 import android.support.test.uiautomator.Direction;
45 import android.support.test.uiautomator.UiDevice;
46 import android.support.test.uiautomator.UiObject2;
47 import android.support.test.uiautomator.UiObjectNotFoundException;
48 import android.support.test.uiautomator.UiScrollable;
49 import android.support.test.uiautomator.UiSelector;
50 import android.support.test.uiautomator.Until;
51 import android.util.ArrayMap;
52 import android.util.Log;
53 import android.view.accessibility.AccessibilityEvent;
54 import android.view.accessibility.AccessibilityNodeInfo;
55 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
56 import android.widget.ScrollView;
57 
58 import androidx.test.InstrumentationRegistry;
59 import androidx.test.runner.AndroidJUnit4;
60 
61 import junit.framework.Assert;
62 
63 import org.junit.Before;
64 import org.junit.runner.RunWith;
65 
66 import java.util.List;
67 import java.util.Map;
68 import java.util.concurrent.Callable;
69 import java.util.concurrent.TimeoutException;
70 import java.util.regex.Pattern;
71 
72 @RunWith(AndroidJUnit4.class)
73 public abstract class BasePermissionsTest {
74     private static final String PLATFORM_PACKAGE_NAME = "android";
75 
76     private static final long IDLE_TIMEOUT_MILLIS = 1000;
77     private static final long GLOBAL_TIMEOUT_MILLIS = 10000;
78 
79     private static final long RETRY_TIMEOUT = 10 * GLOBAL_TIMEOUT_MILLIS;
80     private static final String LOG_TAG = "BasePermissionsTest";
81 
82     private static Map<String, String> sPermissionToLabelResNameMap = new ArrayMap<>();
83 
84     private Context mContext;
85     private Resources mPermissionControllerResources;
86     private Resources mPlatformResources;
87     private boolean mWatch;
88 
getInstrumentation()89     protected static Instrumentation getInstrumentation() {
90         return InstrumentationRegistry.getInstrumentation();
91     }
92 
assertPermissionRequestResult(BasePermissionActivity.Result result, int requestCode, String[] permissions, boolean[] granted)93     protected static void assertPermissionRequestResult(BasePermissionActivity.Result result,
94             int requestCode, String[] permissions, boolean[] granted) {
95         assertEquals(requestCode, result.requestCode);
96         for (int i = 0; i < permissions.length; i++) {
97             assertEquals(permissions[i], result.permissions[i]);
98             assertEquals(granted[i] ? PackageManager.PERMISSION_GRANTED
99                     : PackageManager.PERMISSION_DENIED, result.grantResults[i]);
100 
101         }
102     }
103 
getUiDevice()104     protected static UiDevice getUiDevice() {
105         return UiDevice.getInstance(getInstrumentation());
106     }
107 
launchActivity(String packageName, Class<?> clazz, Bundle extras)108     protected static Activity launchActivity(String packageName,
109             Class<?> clazz, Bundle extras) {
110         Intent intent = new Intent(Intent.ACTION_MAIN);
111         intent.setClassName(packageName, clazz.getName());
112         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
113         if (extras != null) {
114             intent.putExtras(extras);
115         }
116         Activity activity = getInstrumentation().startActivitySync(intent);
117         getInstrumentation().waitForIdleSync();
118 
119         return activity;
120     }
121 
initPermissionToLabelMap(boolean permissionReviewMode)122     private void initPermissionToLabelMap(boolean permissionReviewMode) {
123         if (!permissionReviewMode) {
124             // Contacts
125             sPermissionToLabelResNameMap.put(
126                     Manifest.permission.READ_CONTACTS, "@android:string/permgrouplab_contacts");
127             sPermissionToLabelResNameMap.put(
128                     Manifest.permission.WRITE_CONTACTS, "@android:string/permgrouplab_contacts");
129             // Calendar
130             sPermissionToLabelResNameMap.put(
131                     Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar");
132             sPermissionToLabelResNameMap.put(
133                     Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar");
134             // SMS
135             sPermissionToLabelResNameMap.put(
136                     Manifest.permission.SEND_SMS, "@android:string/permgrouplab_sms");
137             sPermissionToLabelResNameMap.put(
138                     Manifest.permission.RECEIVE_SMS, "@android:string/permgrouplab_sms");
139             sPermissionToLabelResNameMap.put(
140                     Manifest.permission.READ_SMS, "@android:string/permgrouplab_sms");
141             sPermissionToLabelResNameMap.put(
142                     Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permgrouplab_sms");
143             sPermissionToLabelResNameMap.put(
144                     Manifest.permission.RECEIVE_MMS, "@android:string/permgrouplab_sms");
145             sPermissionToLabelResNameMap.put(
146                     "android.permission.READ_CELL_BROADCASTS", "@android:string/permgrouplab_sms");
147             // Storage
148             sPermissionToLabelResNameMap.put(
149                     Manifest.permission.READ_EXTERNAL_STORAGE,
150                     "@android:string/permgrouplab_storage");
151             sPermissionToLabelResNameMap.put(
152                     Manifest.permission.WRITE_EXTERNAL_STORAGE,
153                     "@android:string/permgrouplab_storage");
154             // Location
155             sPermissionToLabelResNameMap.put(
156                     Manifest.permission.ACCESS_FINE_LOCATION,
157                     "@android:string/permgrouplab_location");
158             sPermissionToLabelResNameMap.put(
159                     Manifest.permission.ACCESS_COARSE_LOCATION,
160                     "@android:string/permgrouplab_location");
161             // Phone
162             sPermissionToLabelResNameMap.put(
163                     Manifest.permission.READ_PHONE_STATE, "@android:string/permgrouplab_phone");
164             sPermissionToLabelResNameMap.put(
165                     Manifest.permission.CALL_PHONE, "@android:string/permgrouplab_phone");
166             sPermissionToLabelResNameMap.put(
167                     "android.permission.ACCESS_IMS_CALL_SERVICE",
168                     "@android:string/permgrouplab_phone");
169             sPermissionToLabelResNameMap.put(
170                     Manifest.permission.READ_CALL_LOG, "@android:string/permgrouplab_phone");
171             sPermissionToLabelResNameMap.put(
172                     Manifest.permission.WRITE_CALL_LOG, "@android:string/permgrouplab_phone");
173             sPermissionToLabelResNameMap.put(
174                     Manifest.permission.ADD_VOICEMAIL, "@android:string/permgrouplab_phone");
175             sPermissionToLabelResNameMap.put(
176                     Manifest.permission.USE_SIP, "@android:string/permgrouplab_phone");
177             sPermissionToLabelResNameMap.put(
178                     Manifest.permission.PROCESS_OUTGOING_CALLS,
179                     "@android:string/permgrouplab_phone");
180             // Microphone
181             sPermissionToLabelResNameMap.put(
182                     Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone");
183             // Camera
184             sPermissionToLabelResNameMap.put(
185                     Manifest.permission.CAMERA, "@android:string/permgrouplab_camera");
186             // Body sensors
187             sPermissionToLabelResNameMap.put(
188                     Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors");
189         } else {
190             // Contacts
191             sPermissionToLabelResNameMap.put(
192                     Manifest.permission.READ_CONTACTS, "@android:string/permlab_readContacts");
193             sPermissionToLabelResNameMap.put(
194                     Manifest.permission.WRITE_CONTACTS, "@android:string/permlab_writeContacts");
195             // Calendar
196             sPermissionToLabelResNameMap.put(
197                     Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar");
198             sPermissionToLabelResNameMap.put(
199                     Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar");
200             // SMS
201             sPermissionToLabelResNameMap.put(
202                     Manifest.permission.SEND_SMS, "@android:string/permlab_sendSms");
203             sPermissionToLabelResNameMap.put(
204                     Manifest.permission.RECEIVE_SMS, "@android:string/permlab_receiveSms");
205             sPermissionToLabelResNameMap.put(
206                     Manifest.permission.READ_SMS, "@android:string/permlab_readSms");
207             sPermissionToLabelResNameMap.put(
208                     Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permlab_receiveWapPush");
209             sPermissionToLabelResNameMap.put(
210                     Manifest.permission.RECEIVE_MMS, "@android:string/permlab_receiveMms");
211             sPermissionToLabelResNameMap.put(
212                     "android.permission.READ_CELL_BROADCASTS",
213                     "@android:string/permlab_readCellBroadcasts");
214             // Storage
215             sPermissionToLabelResNameMap.put(
216                     Manifest.permission.READ_EXTERNAL_STORAGE,
217                     "@android:string/permgrouplab_storage");
218             sPermissionToLabelResNameMap.put(
219                     Manifest.permission.WRITE_EXTERNAL_STORAGE,
220                     "@android:string/permgrouplab_storage");
221             // Location
222             sPermissionToLabelResNameMap.put(
223                     Manifest.permission.ACCESS_FINE_LOCATION,
224                     "@android:string/permgrouplab_location");
225             sPermissionToLabelResNameMap.put(
226                     Manifest.permission.ACCESS_COARSE_LOCATION,
227                     "@android:string/permgrouplab_location");
228             // Phone
229             sPermissionToLabelResNameMap.put(
230                     Manifest.permission.READ_PHONE_STATE, "@android:string/permlab_readPhoneState");
231             sPermissionToLabelResNameMap.put(
232                     Manifest.permission.CALL_PHONE, "@android:string/permlab_callPhone");
233             sPermissionToLabelResNameMap.put(
234                     "android.permission.ACCESS_IMS_CALL_SERVICE",
235                     "@android:string/permlab_accessImsCallService");
236             sPermissionToLabelResNameMap.put(
237                     Manifest.permission.READ_CALL_LOG, "@android:string/permlab_readCallLog");
238             sPermissionToLabelResNameMap.put(
239                     Manifest.permission.WRITE_CALL_LOG, "@android:string/permlab_writeCallLog");
240             sPermissionToLabelResNameMap.put(
241                     Manifest.permission.ADD_VOICEMAIL, "@android:string/permlab_addVoicemail");
242             sPermissionToLabelResNameMap.put(
243                     Manifest.permission.USE_SIP, "@android:string/permlab_use_sip");
244             sPermissionToLabelResNameMap.put(
245                     Manifest.permission.PROCESS_OUTGOING_CALLS,
246                     "@android:string/permlab_processOutgoingCalls");
247             // Microphone
248             sPermissionToLabelResNameMap.put(
249                     Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone");
250             // Camera
251             sPermissionToLabelResNameMap.put(
252                     Manifest.permission.CAMERA, "@android:string/permgrouplab_camera");
253             // Body sensors
254             sPermissionToLabelResNameMap.put(
255                     Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors");
256         }
257     }
258 
259     @Before
beforeTest()260     public void beforeTest() throws PackageManager.NameNotFoundException {
261         mContext = InstrumentationRegistry.getTargetContext();
262         try {
263             Context platformContext = mContext.createPackageContext(PLATFORM_PACKAGE_NAME, 0);
264             mPlatformResources = platformContext.getResources();
265 
266         } catch (PackageManager.NameNotFoundException e) {
267             /* cannot happen */
268         }
269 
270         if (isAutomotive()) {
271             Context permissionControllerContext = mContext.createPackageContext(
272                     getPermissionControllerPackageName(), 0);
273             mPermissionControllerResources = permissionControllerContext.getResources();
274             assertNotNull(mPermissionControllerResources);
275         }
276 
277         PackageManager packageManager = mContext.getPackageManager();
278         mWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
279         initPermissionToLabelMap(packageManager.arePermissionsIndividuallyControlled());
280 
281         UiObject2 button = getUiDevice().findObject(By.text("Close"));
282         if (button != null) {
283             button.click();
284         }
285     }
286 
getPermissionControllerPackageName()287     private String getPermissionControllerPackageName() {
288         final Intent intent = new Intent("android.intent.action.MANAGE_PERMISSIONS");
289         intent.addCategory(Intent.CATEGORY_DEFAULT);
290 
291         PackageManager packageManager = mContext.getPackageManager();
292 
293         final List<ResolveInfo> matches = packageManager.queryIntentActivities(intent,
294                 MATCH_SYSTEM_ONLY | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE);
295 
296         if (matches.size() == 1) {
297             ResolveInfo resolveInfo = matches.get(0);
298             if (!resolveInfo.activityInfo.applicationInfo.isPrivilegedApp()) {
299                 throw new RuntimeException("The permissions manager must be a privileged app");
300             }
301             return matches.get(0).activityInfo.packageName;
302         } else {
303             throw new RuntimeException("There must be exactly one permissions manager; found "
304                     + matches);
305         }
306     }
307 
requestPermissions( String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction)308     protected BasePermissionActivity.Result requestPermissions(
309             String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction)
310             throws Exception {
311         // Start an activity
312         BasePermissionActivity activity = (BasePermissionActivity) launchActivity(
313                 getInstrumentation().getTargetContext().getPackageName(), clazz, null);
314 
315         activity.waitForOnCreate();
316 
317         // Request the permissions
318         activity.requestPermissions(permissions, requestCode);
319 
320         // Define a more conservative idle criteria
321         getInstrumentation().getUiAutomation().waitForIdle(
322                 IDLE_TIMEOUT_MILLIS, GLOBAL_TIMEOUT_MILLIS);
323 
324         // Perform the post-request action
325         if (postRequestAction != null) {
326             postRequestAction.run();
327         }
328 
329         BasePermissionActivity.Result result = activity.getResult();
330         activity.finish();
331         return result;
332     }
333 
clickAllowButton()334     protected void clickAllowButton() throws Exception {
335         scrollToBottomIfWatch();
336         waitForIdle();
337         if (isAutomotive()) {
338             clickStringRes("grant_dialog_button_allow");
339         } else {
340             getUiDevice().wait(Until.findObject(By.res(
341                 "com.android.permissioncontroller:id/permission_allow_button")),
342                 GLOBAL_TIMEOUT_MILLIS).click();
343         }
344     }
345 
clickAllowAlwaysButton()346     protected void clickAllowAlwaysButton() throws Exception {
347         waitForIdle();
348         if (isAutomotive()) {
349             clickStringRes("grant_dialog_button_allow_always");
350         } else {
351             getUiDevice().wait(Until.findObject(By.res(
352                     "com.android.permissioncontroller:id/permission_allow_always_button")),
353                     GLOBAL_TIMEOUT_MILLIS).click();
354         }
355     }
356 
clickAllowForegroundButton()357     protected void clickAllowForegroundButton() throws Exception {
358         waitForIdle();
359         if (isAutomotive()) {
360             clickStringRes("grant_dialog_button_allow_foreground");
361         } else {
362             getUiDevice().wait(Until.findObject(By.res(
363                     "com.android.permissioncontroller:id/permission_allow_foreground_only_button")),
364                     GLOBAL_TIMEOUT_MILLIS).click();
365         }
366     }
367 
clickDenyButton()368     protected void clickDenyButton() throws Exception {
369         scrollToBottomIfWatch();
370         waitForIdle();
371         if (isAutomotive()) {
372             clickStringRes("grant_dialog_button_deny");
373             // or "Keep while-in-use access", but that's untested
374         } else {
375             getUiDevice().wait(Until.findObject(By.res(
376                     "com.android.permissioncontroller:id/permission_deny_button")),
377                     GLOBAL_TIMEOUT_MILLIS).click();
378         }
379     }
380 
clickDenyAndDontAskAgainButton()381     protected void clickDenyAndDontAskAgainButton() throws Exception {
382         waitForIdle();
383         if (isAutomotive()) {
384             clickStringRes("grant_dialog_button_deny_and_dont_ask_again");
385             // or "Keep and don't ask again", but that's untested
386         } else {
387             getUiDevice().wait(Until.findObject(By.res(
388                     "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button")),
389                     GLOBAL_TIMEOUT_MILLIS).click();
390         }
391     }
392 
clickDontAskAgainButton()393     protected void clickDontAskAgainButton() throws Exception {
394         scrollToBottomIfWatch();
395         waitForIdle();
396         getUiDevice().wait(Until.findObject(By.res(
397                 "com.android.permissioncontroller:id/permission_deny_dont_ask_again_button")),
398                 GLOBAL_TIMEOUT_MILLIS).click();
399     }
400 
clickStringRes(String res)401     private void clickStringRes(String res) throws TimeoutException, UiObjectNotFoundException {
402         String s = mPermissionControllerResources.getString(mPermissionControllerResources
403                 .getIdentifier(res, "string", "com.android.permissioncontroller"));
404         waitForIdle();
405         getUiDevice().wait(Until.findObject(By.text(
406                 Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE))),
407                 GLOBAL_TIMEOUT_MILLIS).click();
408     }
409 
grantPermission(String permission)410     protected void grantPermission(String permission) throws Exception {
411         grantPermissions(new String[]{permission});
412     }
413 
grantPermissions(String[] permissions)414     protected void grantPermissions(String[] permissions) throws Exception {
415         setPermissionGrantState(permissions, true, false);
416     }
417 
revokePermission(String permission)418     protected void revokePermission(String permission) throws Exception {
419         revokePermissions(new String[] {permission}, false);
420     }
421 
revokePermissions(String[] permissions, boolean legacyApp)422     protected void revokePermissions(String[] permissions, boolean legacyApp) throws Exception {
423         setPermissionGrantState(permissions, false, legacyApp);
424     }
425 
scrollToBottomIfWatch()426     private void scrollToBottomIfWatch() throws Exception {
427         if (mWatch) {
428             getUiDevice().wait(Until.findObject(By.clazz(ScrollView.class)), GLOBAL_TIMEOUT_MILLIS);
429             UiScrollable scrollable =
430                     new UiScrollable(new UiSelector().className(ScrollView.class));
431             if (scrollable.exists()) {
432                 scrollable.flingToEnd(10);
433             }
434         }
435     }
436 
scrollToAndGetTextObject(String text)437     private boolean scrollToAndGetTextObject(String text) {
438         UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true));
439         try {
440             // Swipe far away from the edges to avoid not triggering swipes
441             scroller.setSwipeDeadZonePercentage(0.25);
442             return scroller.scrollTextIntoView(text);
443         } catch (UiObjectNotFoundException e) {
444             return false;
445         }
446     }
447 
setPermissionGrantState(String[] permissions, boolean granted, boolean legacyApp)448     private void setPermissionGrantState(String[] permissions, boolean granted,
449             boolean legacyApp) throws Exception {
450         getUiDevice().pressBack();
451         waitForIdle();
452         getUiDevice().pressBack();
453         waitForIdle();
454         getUiDevice().pressBack();
455         waitForIdle();
456 
457         if (isTv()) {
458             getUiDevice().pressHome();
459             waitForIdle();
460         }
461 
462         // Open the app details settings
463         Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
464         intent.addCategory(Intent.CATEGORY_DEFAULT);
465         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
466         intent.setData(Uri.parse("package:" + mContext.getPackageName()));
467         startActivity(intent);
468 
469         waitForIdle();
470 
471         // Open the permissions UI
472         String label = mContext.getResources().getString(R.string.Permissions);
473         AccessibilityNodeInfo permLabelView = getNodeTimed(() -> findByText(label), true);
474         Assert.assertNotNull("Permissions label should be present", permLabelView);
475 
476         AccessibilityNodeInfo permItemView = findCollectionItem(permLabelView);
477 
478         click(permItemView);
479 
480         waitForIdle();
481 
482         for (String permission : permissions) {
483             // Find the permission screen
484             String permissionLabel = getPermissionLabel(permission);
485             scrollToAndGetTextObject(permissionLabel);
486 
487             UiObject2 permissionView = null;
488             long start = System.currentTimeMillis();
489             while (permissionView == null && start + RETRY_TIMEOUT > System.currentTimeMillis()) {
490                 permissionView = getUiDevice().wait(Until.findObject(By.text(permissionLabel)),
491                         IDLE_TIMEOUT_MILLIS);
492 
493                 if (permissionView == null) {
494                     getUiDevice().findObject(By.scrollable(true))
495                             .scroll(Direction.DOWN, 1);
496                 }
497             }
498 
499             if (!isTv()) {
500                 permissionView.click();
501                 waitForIdle();
502             }
503 
504             String denyLabel = mContext.getResources().getString(R.string.Deny);
505 
506             final boolean wasGranted = isTv() ? false : !getUiDevice().wait(
507                 Until.findObject(By.text(denyLabel)), GLOBAL_TIMEOUT_MILLIS).isChecked();
508             // TV does not use checked state to represent granted state.
509             if (granted != wasGranted || isTv()) {
510                 // Toggle the permission
511 
512                 if (isTv()) {
513                     // no Allow/Deny labels on TV
514                     permissionView.click();
515                 } else if (granted) {
516                     String allowLabel = mContext.getResources().getString(R.string.Allow);
517                     getUiDevice().findObject(By.text(allowLabel)).click();
518                 } else {
519                     getUiDevice().findObject(By.text(denyLabel)).click();
520                 }
521                 waitForIdle();
522 
523                 if (wasGranted && legacyApp) {
524                     scrollToBottomIfWatch();
525                     Context context = getInstrumentation().getContext();
526                     String packageName = context.getPackageManager()
527                             .getPermissionControllerPackageName();
528                     String resIdName = "com.android.permissioncontroller"
529                             + ":string/grant_dialog_button_deny_anyway";
530                     Resources resources = context
531                             .createPackageContext(packageName, 0).getResources();
532                     final int confirmResId = resources.getIdentifier(resIdName, null, null);
533                     String confirmTitle = CaseMap.toUpper().apply(
534                             resources.getConfiguration().getLocales().get(0),
535                             resources.getString(confirmResId));
536                     getUiDevice().wait(Until.findObject(
537                             byTextStartsWithCaseInsensitive(confirmTitle)),
538                             GLOBAL_TIMEOUT_MILLIS).click();
539 
540                     waitForIdle();
541                 }
542             }
543 
544             if (!isTv()) {
545                 getUiDevice().pressBack();
546                 waitForIdle();
547             }
548         }
549 
550         getUiDevice().pressBack();
551         waitForIdle();
552         getUiDevice().pressBack();
553         waitForIdle();
554     }
555 
byTextStartsWithCaseInsensitive(String prefix)556     private BySelector byTextStartsWithCaseInsensitive(String prefix) {
557         return By.text(Pattern.compile(String.format("(?i)^%s.*$", Pattern.quote(prefix))));
558     }
559 
getPermissionLabel(String permission)560     private String getPermissionLabel(String permission) throws Exception {
561         String labelResName = sPermissionToLabelResNameMap.get(permission);
562         assertNotNull("Unknown permisison " + permission, labelResName);
563         final int resourceId = mPlatformResources.getIdentifier(labelResName, null, null);
564         return mPlatformResources.getString(resourceId);
565     }
566 
startActivity(final Intent intent)567     private void startActivity(final Intent intent) throws Exception {
568         getInstrumentation().getUiAutomation().executeAndWaitForEvent(
569                 () -> {
570             try {
571                 getInstrumentation().getContext().startActivity(intent);
572             } catch (Exception e) {
573                 Log.e(LOG_TAG, "Cannot start activity: " + intent, e);
574                 fail("Cannot start activity: " + intent);
575             }
576         }, (AccessibilityEvent event) -> event.getEventType()
577                         == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
578          , GLOBAL_TIMEOUT_MILLIS);
579     }
580 
findByText(String text)581     private AccessibilityNodeInfo findByText(String text) throws Exception {
582         AccessibilityNodeInfo root = getInstrumentation().getUiAutomation().getRootInActiveWindow();
583         AccessibilityNodeInfo result = findByText(root, text);
584         if (result != null) {
585             return result;
586         }
587         return findByTextInCollection(root, text);
588     }
589 
findByText(AccessibilityNodeInfo root, String text)590     private static AccessibilityNodeInfo findByText(AccessibilityNodeInfo root, String text) {
591         if (root == null) {
592             return null;
593         }
594         List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(text);
595         PackageManager packageManager = InstrumentationRegistry.getTargetContext().getPackageManager();
596         boolean isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
597         if (isWatch) {
598             return findByTextForWatch(root, text);
599         }
600         for (AccessibilityNodeInfo node : nodes) {
601             if (node.getText().toString().equals(text)) {
602                 return node;
603             }
604         }
605         return null;
606     }
607 
findByTextForWatch(AccessibilityNodeInfo root, String text)608     private static AccessibilityNodeInfo findByTextForWatch(AccessibilityNodeInfo root, String text) {
609         String trimmedText = trimText(text);
610         List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(trimmedText);
611         for (AccessibilityNodeInfo node : nodes) {
612             if (trimText(node.getText().toString()).equals(trimmedText)) {
613                 return node;
614             }
615         }
616         return null;
617     }
618 
trimText(String text)619     private static String trimText(String text) {
620         return text != null ? text.substring(0, Math.min(text.length(), 20)) : null;
621     }
622 
findByTextInCollection(AccessibilityNodeInfo root, String text)623     private static AccessibilityNodeInfo findByTextInCollection(AccessibilityNodeInfo root,
624             String text)  throws Exception {
625         AccessibilityNodeInfo result;
626         final int childCount = root.getChildCount();
627         for (int i = 0; i < childCount; i++) {
628             AccessibilityNodeInfo child = root.getChild(i);
629             if (child == null) {
630                 continue;
631             }
632             if (child.getCollectionInfo() != null) {
633                 scrollTop(child);
634                 result = getNodeTimed(() -> findByText(child, text), false);
635                 if (result != null) {
636                     return result;
637                 }
638                 try {
639                     while (child.getActionList().contains(
640                             AccessibilityAction.ACTION_SCROLL_FORWARD) || child.getActionList()
641                             .contains(AccessibilityAction.ACTION_SCROLL_DOWN)) {
642                         scrollForward(child);
643                         result = getNodeTimed(() -> findByText(child, text), false);
644                         if (result != null) {
645                             return result;
646                         }
647                     }
648                 } catch (TimeoutException e) {
649                      /* ignore */
650                 }
651             } else {
652                 result = findByTextInCollection(child, text);
653                 if (result != null) {
654                     return result;
655                 }
656             }
657         }
658         return null;
659     }
660 
scrollTop(AccessibilityNodeInfo node)661     private static void scrollTop(AccessibilityNodeInfo node) throws Exception {
662         try {
663             while (node.getActionList().contains(AccessibilityAction.ACTION_SCROLL_BACKWARD)) {
664                 scroll(node, false);
665             }
666         } catch (TimeoutException e) {
667             /* ignore */
668         }
669     }
670 
scrollForward(AccessibilityNodeInfo node)671     private static void scrollForward(AccessibilityNodeInfo node) throws Exception {
672             scroll(node, true);
673     }
674 
scroll(AccessibilityNodeInfo node, boolean forward)675     private static void scroll(AccessibilityNodeInfo node, boolean forward) throws Exception {
676         getInstrumentation().getUiAutomation().executeAndWaitForEvent(
677                 () -> {
678                     if (isTv()) {
679                         if (forward) {
680                             getUiDevice().pressDPadDown();
681                         } else {
682                             for (int i = 0; i < 50; i++) {
683                                 getUiDevice().pressDPadUp();
684                             }
685                         }
686                     } else {
687                         node.performAction(forward
688                                 ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
689                                 : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
690                     }
691                 },
692                 (AccessibilityEvent event) -> event.getEventType()
693                         == AccessibilityEvent.TYPE_VIEW_SCROLLED
694                         || event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
695                 GLOBAL_TIMEOUT_MILLIS);
696         node.refresh();
697         waitForIdle();
698     }
699 
click(AccessibilityNodeInfo node)700     private static void click(AccessibilityNodeInfo node) throws Exception {
701         getInstrumentation().getUiAutomation().executeAndWaitForEvent(
702                 () -> node.performAction(AccessibilityNodeInfo.ACTION_CLICK),
703                 (AccessibilityEvent event) -> event.getEventType()
704                         == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
705                         || event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED,
706                 GLOBAL_TIMEOUT_MILLIS);
707     }
708 
findCollectionItem(AccessibilityNodeInfo current)709     private static AccessibilityNodeInfo findCollectionItem(AccessibilityNodeInfo current)
710             throws Exception {
711         AccessibilityNodeInfo result = current;
712         while (result != null) {
713             // Nodes that are in the hierarchy but not yet on screen may not have collection item
714             // info populated. Use a parent with collection info as an indicator in those cases.
715             if (result.getCollectionItemInfo() != null || hasCollectionAsParent(result)) {
716                 return result;
717             }
718             result = result.getParent();
719         }
720         return null;
721     }
722 
hasCollectionAsParent(AccessibilityNodeInfo node)723     private static boolean hasCollectionAsParent(AccessibilityNodeInfo node) {
724         return node.getParent() != null && node.getParent().getCollectionInfo() != null;
725     }
726 
getNodeTimed( Callable<AccessibilityNodeInfo> callable, boolean retry)727     private static AccessibilityNodeInfo getNodeTimed(
728             Callable<AccessibilityNodeInfo> callable, boolean retry) throws Exception {
729         final long startTimeMillis = SystemClock.uptimeMillis();
730         while (true) {
731             try {
732                 AccessibilityNodeInfo node = callable.call();
733 
734                 if (node != null) {
735                     return node;
736                 }
737             } catch (NullPointerException e) {
738                 Log.e(LOG_TAG, "NPE while finding AccessibilityNodeInfo", e);
739             }
740 
741             final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
742             if (!retry || elapsedTimeMillis > RETRY_TIMEOUT) {
743                 return null;
744             }
745             SystemClock.sleep(2 * elapsedTimeMillis);
746         }
747     }
748 
waitForIdle()749     private static void waitForIdle() throws TimeoutException {
750         getInstrumentation().getUiAutomation().waitForIdle(IDLE_TIMEOUT_MILLIS,
751                 GLOBAL_TIMEOUT_MILLIS);
752     }
753 
isTv()754     private static boolean isTv() {
755         return getInstrumentation().getContext().getPackageManager()
756                 .hasSystemFeature(PackageManager.FEATURE_LEANBACK);
757     }
758 
isAutomotive()759     protected static boolean isAutomotive() {
760         return getInstrumentation().getContext().getPackageManager()
761                 .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
762     }
763  }
764