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