blob: 5d492c6b33aec7eeed28a4158b2b130b31d38f4a [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.permission.cts;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_FOREGROUND;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.permissionToOp;
import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE;
import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.app.AppOpsManager;
import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PermissionInfo;
import android.os.Process;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Common utils for permission tests
*/
public class PermissionUtils {
private static final long TIMEOUT_MILLIS = 10000;
private static final int TESTED_FLAGS = FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_USER_FIXED
| FLAG_PERMISSION_REVOKE_ON_UPGRADE | FLAG_PERMISSION_REVIEW_REQUIRED
| FLAG_PERMISSION_REVOKE_WHEN_REQUESTED;
private static final Context sContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
private static final UiAutomation sUiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
private PermissionUtils() {
// this class should never be instantiated
}
/**
* Get the state of an app-op.
*
* @param packageName The package the app-op belongs to
* @param permission The permission the app-op belongs to
*
* @return The mode the op is on
*/
public static int getAppOp(@NonNull String packageName, @NonNull String permission)
throws Exception {
return callWithShellPermissionIdentity(
() -> sContext.getSystemService(AppOpsManager.class).unsafeCheckOpRaw(
permissionToOp(permission),
sContext.getPackageManager().getPackageUid(packageName, 0), packageName));
}
/**
* Install an APK.
*
* @param apkFile The apk to install
*/
public static void install(@NonNull String apkFile) {
runShellCommand("pm install -r --force-sdk " + apkFile);
}
/**
* Uninstall a package.
*
* @param packageName Name of package to be uninstalled
*/
public static void uninstallApp(@NonNull String packageName) {
runShellCommand("pm uninstall " + packageName);
}
/**
* Set a new state for an app-op (using the permission-name)
*
* @param packageName The package the app-op belongs to
* @param permission The permission the app-op belongs to
* @param mode The new mode
*/
public static void setAppOp(@NonNull String packageName, @NonNull String permission, int mode) {
setAppOpByName(packageName, permissionToOp(permission), mode);
}
/**
* Set a new state for an app-op (using the app-op-name)
*
* @param packageName The package the app-op belongs to
* @param op The name of the op
* @param mode The new mode
*/
public static void setAppOpByName(@NonNull String packageName, @NonNull String op, int mode) {
runWithShellPermissionIdentity(
() -> sContext.getSystemService(AppOpsManager.class).setUidMode(op,
sContext.getPackageManager().getPackageUid(packageName, 0), mode));
}
/**
* Checks a permission. Does <u>not</u> check the appOp.
*
* <p>Users should use {@link #isGranted} instead.
*
* @param packageName The package that might have the permission granted
* @param permission The permission that might be granted
*
* @return {@code true} iff the permission is granted
*/
public static boolean isPermissionGranted(@NonNull String packageName,
@NonNull String permission) throws Exception {
return sContext.checkPermission(permission, Process.myPid(),
sContext.getPackageManager().getPackageUid(packageName, 0))
== PERMISSION_GRANTED;
}
/**
* Checks if a permission is granted for a package.
*
* <p>This correctly handles pre-M apps by checking the app-ops instead.
* <p>This also correctly handles the location background permission, but does not handle any
* other background permission
*
* @param packageName The package that might have the permission granted
* @param permission The permission that might be granted
*
* @return {@code true} iff the permission is granted
*/
public static boolean isGranted(@NonNull String packageName, @NonNull String permission)
throws Exception {
if (!isPermissionGranted(packageName, permission)) {
return false;
}
if (permission.equals(ACCESS_BACKGROUND_LOCATION)) {
// The app-op for background location is encoded into the mode of the foreground
// location
return getAppOp(packageName, ACCESS_COARSE_LOCATION) == MODE_ALLOWED;
} else {
int mode = getAppOp(packageName, permission);
return mode == MODE_ALLOWED || mode == MODE_FOREGROUND;
}
}
/**
* Grant a permission to an app.
*
* <p>This correctly handles pre-M apps by setting the app-ops.
* <p>This also correctly handles the location background permission, but does not handle any
* other background permission
*
* @param packageName The app that should have the permission granted
* @param permission The permission to grant
*/
public static void grantPermission(@NonNull String packageName, @NonNull String permission)
throws Exception {
sUiAutomation.grantRuntimePermission(packageName, permission);
if (permission.equals(ACCESS_BACKGROUND_LOCATION)) {
// The app-op for background location is encoded into the mode of the foreground
// location
if (isPermissionGranted(packageName, ACCESS_COARSE_LOCATION)) {
setAppOp(packageName, ACCESS_COARSE_LOCATION, MODE_ALLOWED);
} else {
setAppOp(packageName, ACCESS_COARSE_LOCATION, MODE_FOREGROUND);
}
} else if (permission.equals(ACCESS_COARSE_LOCATION)) {
// The app-op for location depends on the state of the bg location
if (isPermissionGranted(packageName, ACCESS_BACKGROUND_LOCATION)) {
setAppOp(packageName, ACCESS_COARSE_LOCATION, MODE_ALLOWED);
} else {
setAppOp(packageName, ACCESS_COARSE_LOCATION, MODE_FOREGROUND);
}
} else {
setAppOp(packageName, permission, MODE_ALLOWED);
}
}
/**
* Revoke a permission from an app.
*
* <p>This correctly handles pre-M apps by setting the app-ops.
* <p>This also correctly handles the location background permission, but does not handle any
* other background permission
*
* @param packageName The app that should have the permission revoked
* @param permission The permission to revoke
*/
public static void revokePermission(@NonNull String packageName, @NonNull String permission)
throws Exception {
sUiAutomation.revokeRuntimePermission(packageName, permission);
if (permission.equals(ACCESS_BACKGROUND_LOCATION)) {
// The app-op for background location is encoded into the mode of the foreground
// location
if (isGranted(packageName, ACCESS_COARSE_LOCATION)) {
setAppOp(packageName, ACCESS_COARSE_LOCATION, MODE_FOREGROUND);
}
} else {
setAppOp(packageName, permission, MODE_IGNORED);
}
}
/**
* Clear permission state (not app-op state) of package.
*
* @param packageName Package to clear
*/
public static void clearAppState(@NonNull String packageName) {
runShellCommand("pm clear " + packageName);
}
/**
* Get the flags of a permission.
*
* @param packageName Package the permission belongs to
* @param permission Name of the permission
*
* @return Permission flags
*/
public static int getPermissionFlags(@NonNull String packageName, @NonNull String permission) {
try {
return callWithShellPermissionIdentity(
() -> sContext.getPackageManager().getPermissionFlags(permission, packageName,
UserHandle.getUserHandleForUid(Process.myUid())) & TESTED_FLAGS);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Set the flags of a permission.
*
* @param packageName Package the permission belongs to
* @param permission Name of the permission
* @param mask Mask of permissions to set
* @param flags Permissions to set
*/
public static void setPermissionFlags(@NonNull String packageName, @NonNull String permission,
int mask, int flags) {
runWithShellPermissionIdentity(
() -> sContext.getPackageManager().updatePermissionFlags(permission, packageName,
mask, flags, UserHandle.getUserHandleForUid(Process.myUid())));
}
/**
* Get all permissions an app requests. This includes the split permissions.
*
* @param packageName The package that requests the permissions.
*
* @return The permissions requested by the app
*/
public static @NonNull List<String> getPermissions(@NonNull String packageName)
throws Exception {
PackageInfo appInfo = sContext.getPackageManager().getPackageInfo(packageName,
GET_PERMISSIONS);
return Arrays.asList(appInfo.requestedPermissions);
}
/**
* Get all runtime permissions that an app requests. This includes the split permissions.
*
* @param packageName The package that requests the permissions.
*
* @return The runtime permissions requested by the app
*/
public static @NonNull List<String> getRuntimePermissions(@NonNull String packageName)
throws Exception {
ArrayList<String> runtimePermissions = new ArrayList<>();
for (String perm : getPermissions(packageName)) {
PermissionInfo info = sContext.getPackageManager().getPermissionInfo(perm, 0);
if ((info.getProtection() & PROTECTION_DANGEROUS) != 0) {
runtimePermissions.add(perm);
}
}
return runtimePermissions;
}
public interface ThrowingRunnable extends Runnable {
void runOrThrow() throws Exception;
@Override
default void run() {
try {
runOrThrow();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Make sure that a {@link Runnable} eventually finishes without throwing a {@link
* Exception}.
*
* @param r The {@link Runnable} to run.
*/
public static void eventually(@NonNull ThrowingRunnable r) {
long start = System.currentTimeMillis();
while (true) {
try {
r.run();
return;
} catch (Throwable e) {
if (System.currentTimeMillis() - start < TIMEOUT_MILLIS) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
throw new RuntimeException(e);
}
} else {
throw e;
}
}
}
}
}