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