Allow apps to bulk revoke permissions with the correct semantics
Test: atest --test-mapping frameworks/base/core/java/android/permission/:presubmit Fixes: 120269238 Change-Id: Ib9eb244f1c89c09eee1f39e3abb65c1189f7a6f4
This commit is contained in:
@@ -4609,6 +4609,17 @@ package android.os.storage {
|
||||
|
||||
package android.permission {
|
||||
|
||||
public final class PermissionControllerManager {
|
||||
method public void revokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.util.concurrent.Executor, android.permission.PermissionControllerManager.OnRevokeRuntimePermissionsCallback);
|
||||
field public static final int REASON_INSTALLER_POLICY_VIOLATION = 2; // 0x2
|
||||
field public static final int REASON_MALWARE = 1; // 0x1
|
||||
}
|
||||
|
||||
public static abstract class PermissionControllerManager.OnRevokeRuntimePermissionsCallback {
|
||||
ctor public PermissionControllerManager.OnRevokeRuntimePermissionsCallback();
|
||||
method public abstract void onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>);
|
||||
}
|
||||
|
||||
public abstract class PermissionControllerService extends android.app.Service {
|
||||
ctor public PermissionControllerService();
|
||||
method public final void attachBaseContext(android.content.Context);
|
||||
@@ -4616,6 +4627,7 @@ package android.permission {
|
||||
method public abstract int onCountPermissionApps(java.util.List<java.lang.String>, boolean, boolean);
|
||||
method public abstract java.util.List<android.permission.RuntimePermissionPresentationInfo> onGetAppPermissions(java.lang.String);
|
||||
method public abstract void onRevokeRuntimePermission(java.lang.String, java.lang.String);
|
||||
method public abstract java.util.Map<java.lang.String, java.util.List<java.lang.String>> onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.lang.String);
|
||||
field public static final java.lang.String SERVICE_INTERFACE = "android.permission.PermissionControllerService";
|
||||
}
|
||||
|
||||
|
||||
@@ -985,6 +985,21 @@ package android.os.strictmode {
|
||||
|
||||
}
|
||||
|
||||
package android.permission {
|
||||
|
||||
public final class PermissionControllerManager {
|
||||
method public void revokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.util.concurrent.Executor, android.permission.PermissionControllerManager.OnRevokeRuntimePermissionsCallback);
|
||||
field public static final int REASON_INSTALLER_POLICY_VIOLATION = 2; // 0x2
|
||||
field public static final int REASON_MALWARE = 1; // 0x1
|
||||
}
|
||||
|
||||
public static abstract class PermissionControllerManager.OnRevokeRuntimePermissionsCallback {
|
||||
ctor public PermissionControllerManager.OnRevokeRuntimePermissionsCallback();
|
||||
method public abstract void onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package android.print {
|
||||
|
||||
public final class PrintJobInfo implements android.os.Parcelable {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package android.permission;
|
||||
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Interface for system apps to communication with the permission controller.
|
||||
@@ -24,6 +25,8 @@ import android.os.RemoteCallback;
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IPermissionController {
|
||||
void revokeRuntimePermissions(in Bundle request, boolean doDryRun, int reason,
|
||||
String callerPackageName, in RemoteCallback callback);
|
||||
void getAppPermissions(String packageName, in RemoteCallback callback);
|
||||
void revokeRuntimePermission(String packageName, String permissionName);
|
||||
void countPermissionApps(in List<String> permissionNames, boolean countOnlyGranted,
|
||||
|
||||
@@ -22,46 +22,97 @@ import static com.android.internal.util.Preconditions.checkCollectionElementsNot
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.CallbackExecutor;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.SystemService;
|
||||
import android.annotation.TestApi;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
|
||||
import com.android.internal.infra.AbstractRemoteService;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Interface for communicating with the permission controller from system apps. All UI operations
|
||||
* regarding permissions and any changes to the permission state should flow through this
|
||||
* interface.
|
||||
* Interface for communicating with the permission controller.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@TestApi
|
||||
@SystemApi
|
||||
@SystemService(Context.PERMISSION_CONTROLLER_SERVICE)
|
||||
public final class PermissionControllerManager {
|
||||
private static final String TAG = PermissionControllerManager.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The key for retrieving the result from the returned bundle.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_RESULT =
|
||||
"android.permission.PermissionControllerManager.key.result";
|
||||
|
||||
/** @hide */
|
||||
@IntDef(prefix = { "REASON_" }, value = {
|
||||
REASON_MALWARE,
|
||||
REASON_INSTALLER_POLICY_VIOLATION,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Reason {}
|
||||
|
||||
/** The permissions are revoked because the apps holding the permissions are malware */
|
||||
public static final int REASON_MALWARE = 1;
|
||||
|
||||
/**
|
||||
* The permissions are revoked because the apps holding the permissions violate a policy of the
|
||||
* app that installed it.
|
||||
*
|
||||
* <p>If this reason is used only permissions of apps that are installed by the caller of the
|
||||
* API can be revoked.
|
||||
*/
|
||||
public static final int REASON_INSTALLER_POLICY_VIOLATION = 2;
|
||||
|
||||
/**
|
||||
* Callback for delivering the result of {@link #revokeRuntimePermissions}.
|
||||
*/
|
||||
public abstract static class OnRevokeRuntimePermissionsCallback {
|
||||
/**
|
||||
* The result for {@link #revokeRuntimePermissions}.
|
||||
*
|
||||
* @param revoked The actually revoked permissions as
|
||||
* {@code Map<packageName, List<permission>>}
|
||||
*/
|
||||
public abstract void onRevokeRuntimePermissions(@NonNull Map<String, List<String>> revoked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for delivering the result of {@link #getAppPermissions}.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public interface OnGetAppPermissionResultCallback {
|
||||
/**
|
||||
@@ -75,6 +126,8 @@ public final class PermissionControllerManager {
|
||||
|
||||
/**
|
||||
* Callback for delivering the result of {@link #countPermissionApps}.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public interface OnCountPermissionAppsResultCallback {
|
||||
/**
|
||||
@@ -86,23 +139,61 @@ public final class PermissionControllerManager {
|
||||
void onCountPermissionApps(int numApps);
|
||||
}
|
||||
|
||||
private final @NonNull Context mContext;
|
||||
private final RemoteService mRemoteService;
|
||||
|
||||
/** @hide */
|
||||
public PermissionControllerManager(@NonNull Context context) {
|
||||
Intent intent = new Intent(SERVICE_INTERFACE);
|
||||
intent.setPackage(context.getPackageManager().getPermissionControllerPackageName());
|
||||
ResolveInfo serviceInfo = context.getPackageManager().resolveService(intent, 0);
|
||||
|
||||
mContext = context;
|
||||
mRemoteService = new RemoteService(context,
|
||||
serviceInfo.getComponentInfo().getComponentName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a set of runtime permissions for various apps.
|
||||
*
|
||||
* @param request The permissions to revoke as {@code Map<packageName, List<permission>>}
|
||||
* @param doDryRun Compute the permissions that would be revoked, but not actually revoke them
|
||||
* @param reason Why the permission should be revoked
|
||||
* @param executor Executor on which to invoke the callback
|
||||
* @param callback Callback to receive the result
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
|
||||
public void revokeRuntimePermissions(@NonNull Map<String, List<String>> request,
|
||||
boolean doDryRun, @Reason int reason, @NonNull @CallbackExecutor Executor executor,
|
||||
@NonNull OnRevokeRuntimePermissionsCallback callback) {
|
||||
// Check input to fail immediately instead of inside the async request
|
||||
checkNotNull(executor);
|
||||
checkNotNull(callback);
|
||||
checkNotNull(request);
|
||||
for (Map.Entry<String, List<String>> appRequest : request.entrySet()) {
|
||||
checkNotNull(appRequest.getKey());
|
||||
checkCollectionElementsNotNull(appRequest.getValue(), "permissions");
|
||||
}
|
||||
|
||||
// Check required permission to fail immediately instead of inside the oneway binder call
|
||||
if (mContext.checkSelfPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
throw new SecurityException(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
|
||||
+ " required");
|
||||
}
|
||||
|
||||
mRemoteService.scheduleRequest(new PendingRevokeRuntimePermissionRequest(mRemoteService,
|
||||
request, doDryRun, reason, mContext.getPackageName(), executor, callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the runtime permissions for an app.
|
||||
*
|
||||
* @param packageName The package for which to query.
|
||||
* @param callback Callback to receive the result.
|
||||
* @param handler Handler on which to invoke the callback.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS)
|
||||
public void getAppPermissions(@NonNull String packageName,
|
||||
@@ -119,6 +210,8 @@ public final class PermissionControllerManager {
|
||||
*
|
||||
* @param packageName The package for which to revoke
|
||||
* @param permissionName The permission to revoke
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
|
||||
public void revokeRuntimePermission(@NonNull String packageName,
|
||||
@@ -138,6 +231,8 @@ public final class PermissionControllerManager {
|
||||
* @param countSystem Also count system apps
|
||||
* @param callback Callback to receive the result
|
||||
* @param handler Handler on which to invoke the callback
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS)
|
||||
public void countPermissionApps(@NonNull List<String> permissionNames,
|
||||
@@ -205,6 +300,84 @@ public final class PermissionControllerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for {@link #revokeRuntimePermissions}
|
||||
*/
|
||||
private static final class PendingRevokeRuntimePermissionRequest extends
|
||||
AbstractRemoteService.PendingRequest<RemoteService, IPermissionController> {
|
||||
private final @NonNull Map<String, List<String>> mRequest;
|
||||
private final boolean mDoDryRun;
|
||||
private final int mReason;
|
||||
private final @NonNull String mCallingPackage;
|
||||
private final @NonNull OnRevokeRuntimePermissionsCallback mCallback;
|
||||
|
||||
private final @NonNull RemoteCallback mRemoteCallback;
|
||||
|
||||
private PendingRevokeRuntimePermissionRequest(@NonNull RemoteService service,
|
||||
@NonNull Map<String, List<String>> request, boolean doDryRun,
|
||||
@Reason int reason, @NonNull String callingPackage,
|
||||
@NonNull @CallbackExecutor Executor executor,
|
||||
@NonNull OnRevokeRuntimePermissionsCallback callback) {
|
||||
super(service);
|
||||
|
||||
mRequest = request;
|
||||
mDoDryRun = doDryRun;
|
||||
mReason = reason;
|
||||
mCallingPackage = callingPackage;
|
||||
mCallback = callback;
|
||||
|
||||
mRemoteCallback = new RemoteCallback(result -> executor.execute(() -> {
|
||||
long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Map<String, List<String>> revoked = new ArrayMap<>();
|
||||
try {
|
||||
Bundle bundleizedRevoked = result.getBundle(KEY_RESULT);
|
||||
|
||||
for (String packageName : bundleizedRevoked.keySet()) {
|
||||
Preconditions.checkNotNull(packageName);
|
||||
|
||||
ArrayList<String> permissions =
|
||||
bundleizedRevoked.getStringArrayList(packageName);
|
||||
Preconditions.checkCollectionElementsNotNull(permissions,
|
||||
"permissions");
|
||||
|
||||
revoked.put(packageName, permissions);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not read result when revoking runtime permissions", e);
|
||||
}
|
||||
|
||||
callback.onRevokeRuntimePermissions(revoked);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
|
||||
finish();
|
||||
}
|
||||
}), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTimeout(RemoteService remoteService) {
|
||||
mCallback.onRevokeRuntimePermissions(Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Bundle bundledizedRequest = new Bundle();
|
||||
for (Map.Entry<String, List<String>> appRequest : mRequest.entrySet()) {
|
||||
bundledizedRequest.putStringArrayList(appRequest.getKey(),
|
||||
new ArrayList<>(appRequest.getValue()));
|
||||
}
|
||||
|
||||
try {
|
||||
getService().getServiceInterface().revokeRuntimePermissions(bundledizedRequest,
|
||||
mDoDryRun, mReason, mCallingPackage, mRemoteCallback);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error revoking runtime permission", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for {@link #getAppPermissions}
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package android.permission;
|
||||
|
||||
import static com.android.internal.util.Preconditions.checkArgument;
|
||||
import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
|
||||
@@ -26,12 +27,19 @@ import android.annotation.SystemApi;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteCallback;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This service is meant to be implemented by the app controlling permissions.
|
||||
@@ -59,6 +67,20 @@ public abstract class PermissionControllerService extends Service {
|
||||
mHandler = new Handler(base.getMainLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a set of runtime permissions for various apps.
|
||||
*
|
||||
* @param requests The permissions to revoke as {@code Map<packageName, List<permission>>}
|
||||
* @param doDryRun Compute the permissions that would be revoked, but not actually revoke them
|
||||
* @param reason Why the permission should be revoked
|
||||
* @param callerPackageName The package name of the calling app
|
||||
*
|
||||
* @return the actually removed permissions as {@code Map<packageName, List<permission>>}
|
||||
*/
|
||||
public abstract @NonNull Map<String, List<String>> onRevokeRuntimePermissions(
|
||||
@NonNull Map<String, List<String>> requests, boolean doDryRun,
|
||||
@PermissionControllerManager.Reason int reason, @NonNull String callerPackageName);
|
||||
|
||||
/**
|
||||
* Gets the runtime permissions for an app.
|
||||
*
|
||||
@@ -93,6 +115,41 @@ public abstract class PermissionControllerService extends Service {
|
||||
@Override
|
||||
public final IBinder onBind(Intent intent) {
|
||||
return new IPermissionController.Stub() {
|
||||
@Override
|
||||
public void revokeRuntimePermissions(
|
||||
Bundle bundleizedRequest, boolean doDryRun, int reason,
|
||||
String callerPackageName, RemoteCallback callback) {
|
||||
checkNotNull(bundleizedRequest, "bundleizedRequest");
|
||||
checkNotNull(callerPackageName);
|
||||
checkNotNull(callback);
|
||||
|
||||
Map<String, List<String>> request = new ArrayMap<>();
|
||||
for (String packageName : bundleizedRequest.keySet()) {
|
||||
Preconditions.checkNotNull(packageName);
|
||||
|
||||
ArrayList<String> permissions =
|
||||
bundleizedRequest.getStringArrayList(packageName);
|
||||
Preconditions.checkCollectionElementsNotNull(permissions, "permissions");
|
||||
|
||||
request.put(packageName, permissions);
|
||||
}
|
||||
|
||||
enforceCallingPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, null);
|
||||
|
||||
// Verify callerPackageName
|
||||
try {
|
||||
PackageInfo pkgInfo = getPackageManager().getPackageInfo(callerPackageName, 0);
|
||||
checkArgument(getCallingUid() == pkgInfo.applicationInfo.uid);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
mHandler.sendMessage(obtainMessage(
|
||||
PermissionControllerService::revokeRuntimePermissions,
|
||||
PermissionControllerService.this, request, doDryRun, reason,
|
||||
callerPackageName, callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getAppPermissions(String packageName, RemoteCallback callback) {
|
||||
checkNotNull(packageName, "packageName");
|
||||
@@ -133,6 +190,27 @@ public abstract class PermissionControllerService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private void revokeRuntimePermissions(@NonNull Map<String, List<String>> requests,
|
||||
boolean doDryRun, @PermissionControllerManager.Reason int reason,
|
||||
@NonNull String callerPackageName, @NonNull RemoteCallback callback) {
|
||||
Map<String, List<String>> revoked = onRevokeRuntimePermissions(requests,
|
||||
doDryRun, reason, callerPackageName);
|
||||
|
||||
checkNotNull(revoked);
|
||||
Bundle bundledizedRevoked = new Bundle();
|
||||
for (Map.Entry<String, List<String>> appRevocation : revoked.entrySet()) {
|
||||
checkNotNull(appRevocation.getKey());
|
||||
checkCollectionElementsNotNull(appRevocation.getValue(), "permissions");
|
||||
|
||||
bundledizedRevoked.putStringArrayList(appRevocation.getKey(),
|
||||
new ArrayList<>(appRevocation.getValue()));
|
||||
}
|
||||
|
||||
Bundle result = new Bundle();
|
||||
result.putBundle(PermissionControllerManager.KEY_RESULT, bundledizedRevoked);
|
||||
callback.sendResult(result);
|
||||
}
|
||||
|
||||
private void getAppPermissions(@NonNull String packageName, @NonNull RemoteCallback callback) {
|
||||
List<RuntimePermissionPresentationInfo> permissions = onGetAppPermissions(packageName);
|
||||
if (permissions != null && !permissions.isEmpty()) {
|
||||
|
||||
12
core/java/android/permission/TEST_MAPPING
Normal file
12
core/java/android/permission/TEST_MAPPING
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "CtsPermissionTestCases",
|
||||
"options": [
|
||||
{
|
||||
"include-filter": "android.permission.cts.PermissionControllerTest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user