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