/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cts.usepermission; import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import android.Manifest; import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.Settings; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.By; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.UiSelector; import android.util.ArrayMap; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Switch; import junit.framework.Assert; import org.junit.Before; import org.junit.runner.RunWith; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; @RunWith(AndroidJUnit4.class) public abstract class BasePermissionsTest { private static final String PLATFORM_PACKAGE_NAME = "android"; private static final long IDLE_TIMEOUT_MILLIS = 500; private static final long GLOBAL_TIMEOUT_MILLIS = 5000; private static final long RETRY_TIMEOUT = 5000; private static final String LOG_TAG = "BasePermissionsTest"; private static Map sPermissionToLabelResNameMap = new ArrayMap<>(); static { // Contacts sPermissionToLabelResNameMap.put(Manifest.permission.READ_CONTACTS, "@android:string/permgrouplab_contacts"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CONTACTS, "@android:string/permgrouplab_contacts"); // Calendar sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar"); // SMS sPermissionToLabelResNameMap.put(Manifest.permission.SEND_SMS, "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_SMS, "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.READ_SMS, "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_MMS, "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put("android.permission.READ_CELL_BROADCASTS", "@android:string/permgrouplab_sms"); // Storage sPermissionToLabelResNameMap.put(Manifest.permission.READ_EXTERNAL_STORAGE, "@android:string/permgrouplab_storage"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, "@android:string/permgrouplab_storage"); // Location sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_FINE_LOCATION, "@android:string/permgrouplab_location"); sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_COARSE_LOCATION, "@android:string/permgrouplab_location"); // Phone sPermissionToLabelResNameMap.put(Manifest.permission.READ_PHONE_STATE, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.CALL_PHONE, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put("android.permission.ACCESS_IMS_CALL_SERVICE", "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALL_LOG, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALL_LOG, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.ADD_VOICEMAIL, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.USE_SIP, "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.PROCESS_OUTGOING_CALLS, "@android:string/permgrouplab_phone"); // Microphone sPermissionToLabelResNameMap.put(Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone"); // Camera sPermissionToLabelResNameMap.put(Manifest.permission.CAMERA, "@android:string/permgrouplab_camera"); // Body sensors sPermissionToLabelResNameMap.put(Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors"); } private Context mContext; private Resources mPlatformResources; protected static Instrumentation getInstrumentation() { return InstrumentationRegistry.getInstrumentation(); } protected static void assertPermissionRequestResult(BasePermissionActivity.Result result, int requestCode, String[] permissions, boolean[] granted) { assertEquals(requestCode, result.requestCode); for (int i = 0; i < permissions.length; i++) { assertEquals(permissions[i], result.permissions[i]); assertEquals(granted[i] ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED, result.grantResults[i]); } } protected static UiDevice getUiDevice() { return UiDevice.getInstance(getInstrumentation()); } protected static Activity launchActivity(String packageName, Class clazz, Bundle extras) { Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClassName(packageName, clazz.getName()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (extras != null) { intent.putExtras(extras); } Activity activity = getInstrumentation().startActivitySync(intent); getInstrumentation().waitForIdleSync(); return activity; } @Before public void beforeTest() { mContext = InstrumentationRegistry.getTargetContext(); try { Context platformContext = mContext.createPackageContext(PLATFORM_PACKAGE_NAME, 0); mPlatformResources = platformContext.getResources(); } catch (PackageManager.NameNotFoundException e) { /* cannot happen */ } UiObject2 button = getUiDevice().findObject(By.text("Close")); if (button != null) { button.click(); } } protected BasePermissionActivity.Result requestPermissions( String[] permissions, int requestCode, Class clazz, Runnable postRequestAction) throws Exception { // Start an activity BasePermissionActivity activity = (BasePermissionActivity) launchActivity( getInstrumentation().getTargetContext().getPackageName(), clazz, null); activity.waitForOnCreate(); // Request the permissions activity.requestPermissions(permissions, requestCode); // Define a more conservative idle criteria getInstrumentation().getUiAutomation().waitForIdle( IDLE_TIMEOUT_MILLIS, GLOBAL_TIMEOUT_MILLIS); // Perform the post-request action if (postRequestAction != null) { postRequestAction.run(); } BasePermissionActivity.Result result = activity.getResult(); activity.finish(); return result; } protected void clickAllowButton() throws Exception { getUiDevice().findObject(new UiSelector().resourceId( "com.android.packageinstaller:id/permission_allow_button")).click(); } protected void clickDenyButton() throws Exception { getUiDevice().findObject(new UiSelector().resourceId( "com.android.packageinstaller:id/permission_deny_button")).click(); } protected void clickDontAskAgainCheckbox() throws Exception { getUiDevice().findObject(new UiSelector().resourceId( "com.android.packageinstaller:id/do_not_ask_checkbox")).click(); } protected void clickDontAskAgainButton() throws Exception { getUiDevice().findObject(new UiSelector().resourceId( "com.android.packageinstaller:id/permission_deny_dont_ask_again_button")).click(); } protected void grantPermission(String permission) throws Exception { grantPermissions(new String[]{permission}); } protected void grantPermissions(String[] permissions) throws Exception { setPermissionGrantState(permissions, true, false); } protected void revokePermission(String permission) throws Exception { revokePermissions(new String[] {permission}, false); } protected void revokePermissions(String[] permissions, boolean legacyApp) throws Exception { setPermissionGrantState(permissions, false, legacyApp); } private void setPermissionGrantState(String[] permissions, boolean granted, boolean legacyApp) throws Exception { getUiDevice().pressBack(); waitForIdle(); getUiDevice().pressBack(); waitForIdle(); getUiDevice().pressBack(); waitForIdle(); // Open the app details settings Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setData(Uri.parse("package:" + mContext.getPackageName())); startActivity(intent); waitForIdle(); // Open the permissions UI AccessibilityNodeInfo permLabelView = getNodeTimed(() -> findByText("Permissions")); Assert.assertNotNull("Permissions label should be present", permLabelView); AccessibilityNodeInfo permItemView = findCollectionItem(permLabelView); Assert.assertNotNull("Permissions item should be present", permLabelView); click(permItemView); waitForIdle(); for (String permission : permissions) { // Find the permission toggle String permissionLabel = getPermissionLabel(permission); AccessibilityNodeInfo labelView = getNodeTimed(() -> findByText(permissionLabel)); Assert.assertNotNull("Permission label should be present", labelView); AccessibilityNodeInfo itemView = findCollectionItem(labelView); Assert.assertNotNull("Permission item should be present", itemView); final AccessibilityNodeInfo toggleView = findSwitch(itemView); Assert.assertNotNull("Permission toggle should be present", toggleView); final boolean wasGranted = toggleView.isChecked(); if (granted != wasGranted) { // Toggle the permission if (!itemView.getActionList().contains(AccessibilityAction.ACTION_CLICK)) { click(toggleView); } else { click(itemView); } waitForIdle(); if (wasGranted && legacyApp) { String packageName = getInstrumentation().getContext().getPackageManager() .getPermissionControllerPackageName(); String resIdName = "com.android.packageinstaller" + ":string/grant_dialog_button_deny_anyway"; Resources resources = getInstrumentation().getContext() .createPackageContext(packageName, 0).getResources(); final int confirmResId = resources.getIdentifier(resIdName, null, null); String confirmTitle = resources.getString(confirmResId); UiObject denyAnyway = getUiDevice().findObject(new UiSelector() .text(confirmTitle.toUpperCase())); denyAnyway.click(); waitForIdle(); } } } getUiDevice().pressBack(); waitForIdle(); getUiDevice().pressBack(); waitForIdle(); } private String getPermissionLabel(String permission) throws Exception { String labelResName = sPermissionToLabelResNameMap.get(permission); assertNotNull("Unknown permisison " + permission, labelResName); final int resourceId = mPlatformResources.getIdentifier(labelResName, null, null); return mPlatformResources.getString(resourceId); } private void startActivity(final Intent intent) throws Exception { getInstrumentation().getUiAutomation().executeAndWaitForEvent( () -> { try { getInstrumentation().getContext().startActivity(intent); } catch (Exception e) { fail("Cannot start activity: " + intent); } }, (AccessibilityEvent event) -> event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED , GLOBAL_TIMEOUT_MILLIS); } private AccessibilityNodeInfo findByText(String text) throws Exception { AccessibilityNodeInfo root = getInstrumentation().getUiAutomation().getRootInActiveWindow(); AccessibilityNodeInfo result = findByText(root, text); if (result != null) { return result; } return findByTextInCollection(root, text); } private static AccessibilityNodeInfo findByText(AccessibilityNodeInfo root, String text) { List nodes = root.findAccessibilityNodeInfosByText(text); for (AccessibilityNodeInfo node : nodes) { if (node.getText().toString().equals(text)) { return node; } } return null; } private static AccessibilityNodeInfo findByTextInCollection(AccessibilityNodeInfo root, String text) throws Exception { AccessibilityNodeInfo result; final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { AccessibilityNodeInfo child = root.getChild(i); if (child == null) { continue; } if (child.getCollectionInfo() != null) { scrollTop(child); result = getNodeTimed(() -> findByText(child, text)); if (result != null) { return result; } while (child.getActionList().contains(AccessibilityAction.ACTION_SCROLL_FORWARD)) { scrollForward(child); result = getNodeTimed(() -> findByText(child, text)); if (result != null) { return result; } } } else { result = findByTextInCollection(child, text); if (result != null) { return result; } } } return null; } private static void scrollTop(AccessibilityNodeInfo node) throws Exception { try { while (node.getActionList().contains(AccessibilityAction.ACTION_SCROLL_BACKWARD)) { scroll(node, false); } } catch (TimeoutException e) { /* ignore */ } } private static void scrollForward(AccessibilityNodeInfo node) throws Exception { try { scroll(node, true); } catch (TimeoutException e) { /* ignore */ } } private static void scroll(AccessibilityNodeInfo node, boolean forward) throws Exception { getInstrumentation().getUiAutomation().executeAndWaitForEvent( () -> node.performAction(forward ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD), (AccessibilityEvent event) -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED, GLOBAL_TIMEOUT_MILLIS); waitForIdle(); } private static void click(AccessibilityNodeInfo node) throws Exception { getInstrumentation().getUiAutomation().executeAndWaitForEvent( () -> node.performAction(AccessibilityNodeInfo.ACTION_CLICK), (AccessibilityEvent event) -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED, GLOBAL_TIMEOUT_MILLIS); } private static AccessibilityNodeInfo findCollectionItem(AccessibilityNodeInfo current) throws Exception { AccessibilityNodeInfo result = current; while (result != null) { if (result.getCollectionItemInfo() != null) { return result; } result = result.getParent(); } return null; } private static AccessibilityNodeInfo findSwitch(AccessibilityNodeInfo root) throws Exception { if (Switch.class.getName().equals(root.getClassName().toString())) { return root; } final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { AccessibilityNodeInfo child = root.getChild(i); if (child == null) { continue; } if (Switch.class.getName().equals(child.getClassName().toString())) { return child; } AccessibilityNodeInfo result = findSwitch(child); if (result != null) { return result; } } return null; } private static AccessibilityNodeInfo getNodeTimed( Callable callable) throws Exception { final long startTimeMillis = SystemClock.uptimeMillis(); while (true) { try { AccessibilityNodeInfo node = callable.call(); if (node != null) { return node; } } catch (NullPointerException e) { Log.e(LOG_TAG, "NPE while finding AccessibilityNodeInfo", e); } final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; if (elapsedTimeMillis > RETRY_TIMEOUT) { return null; } SystemClock.sleep(2 * elapsedTimeMillis); } } private static void waitForIdle() throws TimeoutException { getInstrumentation().getUiAutomation().waitForIdle(IDLE_TIMEOUT_MILLIS, GLOBAL_TIMEOUT_MILLIS); } }