1 /*
2  * Copyright (C) 2020 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.devicepolicy;
18 
19 import static android.Manifest.permission.CAMERA;
20 import static android.Manifest.permission.RECORD_AUDIO;
21 import static android.content.pm.PackageManager.PERMISSION_DENIED;
22 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
23 
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static org.junit.Assert.fail;
27 
28 import android.Manifest;
29 import android.app.AppOpsManager;
30 import android.app.admin.DevicePolicyManager;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager;
36 import android.os.Process;
37 import android.support.test.uiautomator.By;
38 import android.support.test.uiautomator.BySelector;
39 import android.support.test.uiautomator.UiDevice;
40 import android.support.test.uiautomator.UiObject2;
41 import android.support.test.uiautomator.Until;
42 import android.util.Log;
43 
44 import androidx.test.core.app.ApplicationProvider;
45 import androidx.test.platform.app.InstrumentationRegistry;
46 
47 import java.lang.reflect.Field;
48 import java.lang.reflect.Modifier;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Set;
54 import java.util.regex.Pattern;
55 
56 public class PermissionUtils {
57     private static final String LOG_TAG = PermissionUtils.class.getSimpleName();
58     private static final Set<String> LOCATION_PERMISSIONS = new HashSet<String>();
59 
60     private static final Context sContext = ApplicationProvider.getApplicationContext();
61 
62     static {
63         LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_FINE_LOCATION);
64         LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
65         LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_COARSE_LOCATION);
66     }
67 
68     private static final String ACTION_CHECK_HAS_PERMISSION
69             = "com.android.cts.permission.action.CHECK_HAS_PERMISSION";
70     private static final String ACTION_REQUEST_PERMISSION
71             = "com.android.cts.permission.action.REQUEST_PERMISSION";
72     private static final String EXTRA_PERMISSION = "com.android.cts.permission.extra.PERMISSION";
73 
launchActivityAndCheckPermission(PermissionBroadcastReceiver receiver, String permission, int expected, String packageName, String activityName)74     public static void launchActivityAndCheckPermission(PermissionBroadcastReceiver receiver,
75             String permission, int expected, String packageName, String activityName)
76             throws Exception {
77         launchActivityWithAction(permission, ACTION_CHECK_HAS_PERMISSION,
78                 packageName, activityName);
79         assertBroadcastReceived(receiver, expected);
80     }
81 
assertBroadcastReceived(PermissionBroadcastReceiver receiver, int expected)82     private static void assertBroadcastReceived(PermissionBroadcastReceiver receiver,
83             int expected) throws Exception {
84         int actual = receiver.waitForBroadcast();
85         assertWithMessage("value returned by %s (%s=%s, %s=%s)", receiver,
86                 expected, permissionToString(expected),
87                 actual, permissionToString(actual))
88                         .that(actual).isEqualTo(expected);
89     }
90 
launchActivityAndRequestPermission(PermissionBroadcastReceiver receiver, String permission, int expected, String packageName, String activityName)91     public static void launchActivityAndRequestPermission(PermissionBroadcastReceiver receiver,
92             String permission, int expected, String packageName, String activityName)
93             throws Exception {
94         launchActivityWithAction(permission, ACTION_REQUEST_PERMISSION,
95                 packageName, activityName);
96         assertBroadcastReceived(receiver, expected);
97     }
98 
launchActivityAndRequestPermission(PermissionBroadcastReceiver receiver, UiDevice device, String permission, int expected, String packageName, String activityName)99     public static void launchActivityAndRequestPermission(PermissionBroadcastReceiver
100             receiver, UiDevice device, String permission, int expected,
101             String packageName, String activityName) throws Exception {
102         final List<String> resNames = new ArrayList<>();
103         switch(expected) {
104             case PERMISSION_DENIED:
105                 resNames.add("permission_deny_button");
106                 resNames.add("permission_deny_and_dont_ask_again_button");
107                 break;
108             case PERMISSION_GRANTED:
109                 resNames.add("permission_allow_button");
110                 // For some permissions, different buttons may be available.
111                 if (LOCATION_PERMISSIONS.contains(permission)
112                         || RECORD_AUDIO.equals(permission)
113                         || CAMERA.equals(permission)) {
114                     resNames.add("permission_allow_foreground_only_button");
115                     resNames.add("permission_allow_one_time_button");
116                 }
117                 break;
118             default:
119                 throw new IllegalArgumentException("Invalid expected permission");
120         }
121         launchActivityWithAction(permission, ACTION_REQUEST_PERMISSION,
122                 packageName, activityName);
123         pressPermissionPromptButton(device, expected, resNames.toArray(new String[0]));
124         assertBroadcastReceived(receiver, expected);
125     }
126 
launchActivityWithAction(String permission, String action, String packageName, String activityName)127     private static void launchActivityWithAction(String permission, String action,
128             String packageName, String activityName) {
129         Intent launchIntent = new Intent();
130         launchIntent.setComponent(new ComponentName(packageName, activityName));
131         launchIntent.putExtra(EXTRA_PERMISSION, permission);
132         launchIntent.setAction(action);
133         launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
134         Log.d(LOG_TAG, "Launching activity (with intent " + launchIntent + ") for permission "
135                 + permission + " on uid " + Process.myUid());
136         getContext().startActivity(launchIntent);
137     }
138 
checkPermission(String permission, int expected, String packageName)139     public static void checkPermission(String permission, int expected, String packageName) {
140         assertPermission(permission, packageName, getContext().getPackageManager()
141                 .checkPermission(permission, packageName), expected);
142     }
143 
assertPermission(String permission, String packageName, int actual, int expected)144     private static void assertPermission(String permission, String packageName, int actual,
145             int expected) {
146         assertWithMessage("Wrong status for permission %s on package %s", permission, packageName)
147                 .that(actual).isEqualTo(expected);
148     }
149 
150     /**
151      * Correctly check a runtime permission. This also works for pre-m apps.
152      */
checkPermissionAndAppOps(String permission, int expected, String packageName)153     public static void checkPermissionAndAppOps(String permission, int expected, String packageName)
154             throws Exception {
155         assertPermission(permission, packageName, checkPermissionAndAppOps(permission, packageName),
156                 expected);
157     }
158 
checkPermissionAndAppOps(String permission, String packageName)159     private static int checkPermissionAndAppOps(String permission, String packageName)
160             throws Exception {
161         PackageInfo packageInfo = getContext().getPackageManager().getPackageInfo(packageName, 0);
162         if (getContext().checkPermission(permission, -1, packageInfo.applicationInfo.uid)
163                 == PERMISSION_DENIED) {
164             return PERMISSION_DENIED;
165         }
166 
167         AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
168         if (appOpsManager != null && appOpsManager.noteProxyOpNoThrow(
169                 AppOpsManager.permissionToOp(permission), packageName,
170                 packageInfo.applicationInfo.uid, null, null)
171                 != AppOpsManager.MODE_ALLOWED) {
172             return PERMISSION_DENIED;
173         }
174 
175         return PERMISSION_GRANTED;
176     }
177 
getContext()178     public static Context getContext() {
179         return InstrumentationRegistry.getInstrumentation().getContext();
180     }
181 
pressPermissionPromptButton(UiDevice device, int expectedAction, String[] resNames)182     private static void pressPermissionPromptButton(UiDevice device, int expectedAction,
183             String[] resNames) {
184         UiObject2 button = findPermissionPromptButton(device, expectedAction, resNames);
185         Log.d(LOG_TAG, "Clicking on '" + button.getText() + "'");
186         button.click();
187     }
188 
findPermissionPromptButton(UiDevice device, int expectedAction, String[] resNames)189     private static UiObject2 findPermissionPromptButton(UiDevice device, int expectedAction,
190             String[] resNames) {
191         if ((resNames == null) || (resNames.length == 0)) {
192             throw new IllegalArgumentException("resNames must not be null or empty");
193         }
194         String action;
195         switch (expectedAction) {
196             case PERMISSION_DENIED:
197                 action = "PERMISSION_DENIED";
198                 break;
199             case PERMISSION_GRANTED:
200                 action = "PERMISSION_GRANTED";
201                 break;
202             default:
203                 throw new IllegalArgumentException("Invalid expected action: "
204                         + expectedAction);
205         }
206 
207         // The dialog was moved from the packageinstaller to the permissioncontroller.
208         // Search in multiple packages so the test is not affixed to a particular package.
209         String[] possiblePackages = new String[]{
210                 "com.android.permissioncontroller.permission.ui",
211                 "com.android.packageinstaller",
212                 "com.android.permissioncontroller"};
213 
214         Log.v(LOG_TAG, "findPermissionPromptButton(): pkgs= " + Arrays.toString(possiblePackages)
215                 + ", action=" + action + ", resIds=" + Arrays.toString(resNames));
216         for (String resName : resNames) {
217             for (String possiblePkg : possiblePackages) {
218                 BySelector selector = By
219                         .clazz(android.widget.Button.class.getName())
220                         .res(possiblePkg, resName);
221                 Log.v(LOG_TAG, "trying " + selector);
222                 device.wait(Until.hasObject(selector), 5000);
223                 UiObject2 button = device.findObject(selector);
224                 Log.d(LOG_TAG, String.format("Resource %s in Package %s found? %b", resName,
225                         possiblePkg, button != null));
226                 if (button != null) {
227                     return button;
228                 }
229             }
230         }
231 
232         if (!sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
233             fail("Did not find button for action " + action + " on packages "
234                     + Arrays.toString(possiblePackages));
235         }
236         return findPermissionPromptButtonAutomotive(device, expectedAction);
237     }
238 
findPermissionPromptButtonAutomotive(UiDevice device, int expectedAction)239     private static UiObject2 findPermissionPromptButtonAutomotive(UiDevice device,
240             int expectedAction) {
241         // TODO: ideally the UI should use a more specific resource, so it doesn't need to search
242         // for text
243         Pattern resPattern = Pattern.compile(".*car_ui_list_item_title");
244         Pattern textPattern;
245         String action;
246         switch (expectedAction) {
247             case PERMISSION_DENIED:
248                 action = "PERMISSION_DENIED";
249                 textPattern = Pattern.compile("^Don’t allow$");
250                 break;
251             case PERMISSION_GRANTED:
252                 action = "PERMISSION_GRANTED";
253                 textPattern = Pattern.compile("^Allow|While using the app$");
254                 break;
255             default:
256                 throw new IllegalArgumentException("Invalid expected action: " + expectedAction);
257         }
258         Log.i(LOG_TAG, "Button not found on automotive build; searching for " + resPattern
259                 + " res and " + textPattern + " text instead");
260         BySelector selector = By
261                 .clazz(android.widget.TextView.class.getName())
262                  .text(textPattern)
263                 .res(resPattern);
264         Log.v(LOG_TAG, "selector: " + selector);
265         device.wait(Until.hasObject(selector), 5000);
266         UiObject2 button = device.findObject(selector);
267         Log.d(LOG_TAG, "button: " + button + (button == null ? "" : " (" + button.getText() + ")"));
268         assertWithMessage("Found button with res %s and text '%s'", resPattern, textPattern)
269                 .that(button).isNotNull();
270 
271         return button;
272     }
273 
permissionGrantStateToString(int state)274     public static String permissionGrantStateToString(int state) {
275         return constantToString(DevicePolicyManager.class, "PERMISSION_GRANT_STATE_", state);
276     }
277 
permissionPolicyToString(int policy)278     public static String permissionPolicyToString(int policy) {
279         return constantToString(DevicePolicyManager.class, "PERMISSION_POLICY_", policy);
280     }
281 
permissionToString(int permission)282     public static String permissionToString(int permission) {
283         return constantToString(PackageManager.class, "PERMISSION_", permission);
284     }
285 
286     // Copied from DebugUtils
constantToString(Class<?> clazz, String prefix, int value)287     private static String constantToString(Class<?> clazz, String prefix, int value) {
288         for (Field field : clazz.getDeclaredFields()) {
289             final int modifiers = field.getModifiers();
290             try {
291                 if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)
292                         && field.getType().equals(int.class) && field.getName().startsWith(prefix)
293                         && field.getInt(null) == value) {
294                     return constNameWithoutPrefix(prefix, field);
295                 }
296             } catch (IllegalAccessException ignored) {
297             }
298         }
299         return prefix + Integer.toString(value);
300     }
301 
302     // Copied from DebugUtils
constNameWithoutPrefix(String prefix, Field field)303     private static String constNameWithoutPrefix(String prefix, Field field) {
304         return field.getName().substring(prefix.length());
305     }
306 }
307