Add app exemptions APIs for managing platform restriction exemptions
Add DPM.setApplicationExemptions API along with exemption constants.
Add DPM.getApplicationExemptions API
Add corresponding methods in DPMS along with map of DPM exemptions
constants to app-ops.
Add permission to AndroidManifest to expose to CTS tests.
Bug: 246330879
Test: atest ApplicationExemptionsTest
Change-Id: I1271db694af4091ead951d0167edac55ace92f47
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ce1eff1..dfb67416 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1109,6 +1109,7 @@
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException;
method @Nullable public android.content.Intent createProvisioningIntentFromNfcIntent(@NonNull android.content.Intent);
method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void finalizeWorkProfileProvisioning(@NonNull android.os.UserHandle, @Nullable android.accounts.Account);
+ method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public java.util.Set<java.lang.Integer> getApplicationExemptions(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public boolean getBluetoothContactSharingDisabled(@NonNull android.os.UserHandle);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getDeviceOwner();
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}) public android.content.ComponentName getDeviceOwnerComponentOnAnyUser();
@@ -1134,6 +1135,7 @@
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException;
method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException;
+ method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException;
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setDeviceProvisioningConfigApplied();
method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setDpcDownloaded(boolean);
method @Deprecated @RequiresPermission(value=android.Manifest.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS, conditional=true) public void setProfileOwnerCanAccessDeviceIds(@NonNull android.content.ComponentName);
@@ -1155,6 +1157,7 @@
field public static final String ACTION_SET_PROFILE_OWNER = "android.app.action.SET_PROFILE_OWNER";
field @Deprecated public static final String ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE";
field @RequiresPermission(android.Manifest.permission.LAUNCH_DEVICE_MANAGER_SETUP) public static final String ACTION_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER = "android.app.action.UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER";
+ field public static final int EXEMPT_FROM_APP_STANDBY = 0; // 0x0
field public static final String EXTRA_FORCE_UPDATE_ROLE_HOLDER = "android.app.extra.FORCE_UPDATE_ROLE_HOLDER";
field public static final String EXTRA_LOST_MODE_LOCATION = "android.app.extra.LOST_MODE_LOCATION";
field public static final String EXTRA_PROFILE_OWNER_NAME = "android.app.extra.PROFILE_OWNER_NAME";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 6fedb41..be4df9d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -103,6 +103,7 @@
import com.android.internal.infra.AndroidFuture;
import com.android.internal.net.NetworkUtilsInternal;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.android.org.conscrypt.TrustedCertificateStore;
@@ -3823,6 +3824,27 @@
public static final int OPERATION_SAFETY_REASON_DRIVING_DISTRACTION = 1;
/**
+ * Prevent an app from being placed into app standby buckets, such that it will not be subject
+ * to device resources restrictions as a result of app standby buckets.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int EXEMPT_FROM_APP_STANDBY = 0;
+
+ /**
+ * Exemptions to platform restrictions, given to an application through
+ * {@link #setApplicationExemptions(String, Set)}.
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "EXEMPT_FROM_"}, value = {
+ EXEMPT_FROM_APP_STANDBY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ApplicationExemptionConstants {}
+
+ /**
* Broadcast action: notify system apps (e.g. settings, SysUI, etc) that the device management
* resources with IDs {@link #EXTRA_RESOURCE_IDS} has been updated, the updated resources can be
* retrieved using {@link DevicePolicyResourcesManager#getDrawable} and
@@ -14727,6 +14749,95 @@
}
/**
+ * Service-specific error code used in {@link #setApplicationExemptions(String, Set)} and
+ * {@link #getApplicationExemptions(String)}.
+ * @hide
+ */
+ public static final int ERROR_PACKAGE_NAME_NOT_FOUND = 1;
+
+ /**
+ * Called by an application with the
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission, to
+ * grant platform restriction exemptions to a given application.
+ *
+ * @param packageName The package name of the application to be exempt.
+ * @param exemptions The set of exemptions to be applied.
+ * @throws SecurityException If the caller does not have
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS}
+ * @throws NameNotFoundException If either the package is not installed or the package is not
+ * visible to the caller.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)
+ public void setApplicationExemptions(@NonNull String packageName,
+ @NonNull @ApplicationExemptionConstants Set<Integer> exemptions)
+ throws NameNotFoundException {
+ throwIfParentInstance("setApplicationExemptions");
+ if (mService != null) {
+ try {
+ mService.setApplicationExemptions(packageName,
+ ArrayUtils.convertToIntArray(new ArraySet<>(exemptions)));
+ } catch (ServiceSpecificException e) {
+ switch (e.errorCode) {
+ case ERROR_PACKAGE_NAME_NOT_FOUND:
+ throw new NameNotFoundException(e.getMessage());
+ default:
+ throw new RuntimeException(
+ "Unknown error setting application exemptions: " + e.errorCode, e);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns all the platform restriction exemptions currently applied to an application. Called
+ * by an application with the
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission.
+ *
+ * @param packageName The package name to check.
+ * @return A set of platform restrictions an application is exempt from.
+ * @throws SecurityException If the caller does not have
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS}
+ * @throws NameNotFoundException If either the package is not installed or the package is not
+ * visible to the caller.
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)
+ public Set<Integer> getApplicationExemptions(@NonNull String packageName)
+ throws NameNotFoundException {
+ throwIfParentInstance("getApplicationExemptions");
+ if (mService == null) {
+ return Collections.emptySet();
+ }
+ try {
+ return intArrayToSet(mService.getApplicationExemptions(packageName));
+ } catch (ServiceSpecificException e) {
+ switch (e.errorCode) {
+ case ERROR_PACKAGE_NAME_NOT_FOUND:
+ throw new NameNotFoundException(e.getMessage());
+ default:
+ throw new RuntimeException(
+ "Unknown error getting application exemptions: " + e.errorCode, e);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private Set<Integer> intArrayToSet(int[] array) {
+ Set<Integer> set = new ArraySet<>();
+ for (int item : array) {
+ set.add(item);
+ }
+ return set;
+ }
+
+ /**
* Called by a device owner or a profile owner to disable user control over apps. User will not
* be able to clear app data or force-stop packages. When called by a device owner, applies to
* all users on the device. Starting from Android 13, packages with user control disabled are
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 6c27dd7..8a40265 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -568,4 +568,7 @@
boolean shouldAllowBypassingDevicePolicyManagementRoleQualification();
List<UserHandle> getPolicyManagedProfiles(in UserHandle userHandle);
+
+ void setApplicationExemptions(String packageName, in int[]exemptions);
+ int[] getApplicationExemptions(String packageName);
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 2e4a245..01c0809 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -771,6 +771,9 @@
<!-- Permissions required for CTS test - CtsAppFgsTestCases -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
+ <!-- Permission required for CTS test - ApplicationExemptionsTests -->
+ <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" />
+
<application android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
android:defaultToDeviceProtectedStorage="true"
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 89cbf53..c58e8d5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -23,6 +23,7 @@
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY;
import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED;
import static android.app.admin.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE;
import static android.app.admin.DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE;
@@ -46,6 +47,7 @@
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER;
+import static android.app.admin.DevicePolicyManager.EXEMPT_FROM_APP_STANDBY;
import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE;
import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_IDS;
import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE;
@@ -330,6 +332,7 @@
import android.util.AtomicFile;
import android.util.DebugUtils;
import android.util.IndentingPrintWriter;
+import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -671,6 +674,17 @@
private @interface CopyAccountStatus {}
/**
+ * Mapping of {@link android.app.admin.DevicePolicyManager.ApplicationExemptionConstants} to
+ * corresponding app-ops.
+ */
+ private static final Map<Integer, String> APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS =
+ new ArrayMap<>();
+ static {
+ APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.put(
+ EXEMPT_FROM_APP_STANDBY, OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY);
+ }
+
+ /**
* Admin apps targeting Android S+ may not use
* {@link android.app.admin.DevicePolicyManager#setPasswordQuality} to set password quality
* on the {@code DevicePolicyManager} instance obtained by calling
@@ -17016,6 +17030,88 @@
});
}
+ @Override
+ public void setApplicationExemptions(String packageName, int[] exemptions) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty.");
+ Objects.requireNonNull(exemptions, "Application exemptions must not be null.");
+ Preconditions.checkArgument(areApplicationExemptionsValid(exemptions),
+ "Invalid application exemption constant found in application exemptions set.");
+ Preconditions.checkCallAuthorization(
+ hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS));
+
+ final CallerIdentity caller = getCallerIdentity();
+ final ApplicationInfo packageInfo;
+ packageInfo = getPackageInfoWithNullCheck(packageName, caller);
+
+ for (Map.Entry<Integer, String> entry :
+ APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) {
+ int currentMode = mInjector.getAppOpsManager().unsafeCheckOpNoThrow(
+ entry.getValue(), packageInfo.uid, packageInfo.packageName);
+ int newMode = ArrayUtils.contains(exemptions, entry.getKey())
+ ? MODE_ALLOWED : MODE_DEFAULT;
+ mInjector.binderWithCleanCallingIdentity(() -> {
+ if (currentMode != newMode) {
+ mInjector.getAppOpsManager()
+ .setMode(entry.getValue(),
+ packageInfo.uid,
+ packageName,
+ newMode);
+ }
+ });
+ }
+ }
+
+ @Override
+ public int[] getApplicationExemptions(String packageName) {
+ if (!mHasFeature) {
+ return new int[0];
+ }
+ Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty.");
+ Preconditions.checkCallAuthorization(
+ hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS));
+
+ final CallerIdentity caller = getCallerIdentity();
+ final ApplicationInfo packageInfo;
+ packageInfo = getPackageInfoWithNullCheck(packageName, caller);
+
+ IntArray appliedExemptions = new IntArray(0);
+ for (Map.Entry<Integer, String> entry :
+ APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) {
+ if (mInjector.getAppOpsManager().unsafeCheckOpNoThrow(
+ entry.getValue(), packageInfo.uid, packageInfo.packageName) == MODE_ALLOWED) {
+ appliedExemptions.add(entry.getKey());
+ }
+ }
+ return appliedExemptions.toArray();
+ }
+
+ private ApplicationInfo getPackageInfoWithNullCheck(String packageName, CallerIdentity caller) {
+ final ApplicationInfo packageInfo =
+ mInjector.getPackageManagerInternal().getApplicationInfo(
+ packageName,
+ /* flags= */ 0,
+ caller.getUid(),
+ caller.getUserId());
+ if (packageInfo == null) {
+ throw new ServiceSpecificException(
+ DevicePolicyManager.ERROR_PACKAGE_NAME_NOT_FOUND,
+ "Package name not found.");
+ }
+ return packageInfo;
+ }
+
+ private boolean areApplicationExemptionsValid(int[] exemptions) {
+ for (int exemption : exemptions) {
+ if (!APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.containsKey(exemption)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private boolean isCallingFromPackage(String packageName, int callingUid) {
return mInjector.binderWithCleanCallingIdentity(() -> {
try {