Allow the supervision app to access certain UsageStats APIs.
Allow the active supervision component to access app usage limit APIs.
This includes #registerAppUsageLimitObserver
and #unregisterAppUsageLimitObserver.
Bug: 138681869
Test: atest com.android.server.devicepolicy.DevicePolicyManagerTest#testIsActiveSupervisor
Test: atest AppTimeLimitControllerTests
Test: atest android.app.usage.cts.UsageStatsTest
Change-Id: I3daafc14ccdbf27558c1325b965f2bc6d2dab2f6
diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java
index b873be3..713126e 100644
--- a/core/java/android/app/admin/DevicePolicyManagerInternal.java
+++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java
@@ -24,6 +24,10 @@
/**
* Device policy manager local system service interface.
*
+ * Maintenance note: if you need to expose information from DPMS to lower level services such as
+ * PM/UM/AM/etc, then exposing it from DevicePolicyManagerInternal is not safe because it may cause
+ * lock order inversion. Consider using {@link DevicePolicyCache} instead.
+ *
* @hide Only for use within the system server.
*/
public abstract class DevicePolicyManagerInternal {
@@ -81,6 +85,16 @@
public abstract boolean isActiveAdminWithPolicy(int uid, int reqPolicy);
/**
+ * Checks if an app with given uid is the active supervision admin.
+ *
+ * <p>This takes the DPMS lock. DO NOT call from PM/UM/AM with their lock held.
+ *
+ * @param uid App uid.
+ * @return true if the uid is the active supervision app.
+ */
+ public abstract boolean isActiveSupervisionApp(int uid);
+
+ /**
* Creates an intent to show the admin support dialog to say that an action is disallowed by
* the device/profile owner.
*
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 656f474..1f13a1e 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -801,8 +801,8 @@
* {@link #EXTRA_TIME_USED}. Cannot be {@code null} unless the observer is
* being registered with a {@code timeUsed} equal to or greater than
* {@code timeLimit}.
- * @throws SecurityException if the caller doesn't have both SUSPEND_APPS and OBSERVE_APP_USAGE
- * permissions.
+ * @throws SecurityException if the caller is neither the active supervision app nor does it
+ * have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions.
* @hide
*/
@SystemApi
@@ -827,8 +827,8 @@
* an observer that was already unregistered or never registered will have no effect.
*
* @param observerId The id of the observer that was previously registered.
- * @throws SecurityException if the caller doesn't have both SUSPEND_APPS and OBSERVE_APP_USAGE
- * permissions.
+ * @throws SecurityException if the caller is neither the active supervision app nor does it
+ * have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions.
* @hide
*/
@SystemApi
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c3ff285..05e8380 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -11320,6 +11320,28 @@
}
}
+ @Override
+ public boolean isActiveSupervisionApp(int uid) {
+ synchronized (getLockObject()) {
+ final ActiveAdmin admin = getActiveAdminWithPolicyForUidLocked(
+ null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, uid);
+ if (admin == null) {
+ return false;
+ }
+
+ final String supervisionString = mContext.getResources().getString(
+ com.android.internal.R.string
+ .config_defaultSupervisionProfileOwnerComponent);
+ if (supervisionString == null) {
+ return false;
+ }
+
+ final ComponentName supervisorComponent = ComponentName.unflattenFromString(
+ supervisionString);
+ return admin.info.getComponent().equals(supervisorComponent);
+ }
+ }
+
private void notifyCrossProfileProvidersChanged(int userId, List<String> packages) {
final List<OnCrossProfileWidgetProvidersChangeListener> listeners;
synchronized (getLockObject()) {
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index a25e40f..9a1fd9c 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -2650,6 +2650,21 @@
verifyStayOnWhilePluggedCleared(false);
}
+ public void testIsActiveSupervisionApp() throws Exception {
+ when(mServiceContext.resources
+ .getString(R.string.config_defaultSupervisionProfileOwnerComponent))
+ .thenReturn(admin1.flattenToString());
+
+ final int PROFILE_USER = 15;
+ final int PROFILE_ADMIN = UserHandle.getUid(PROFILE_USER, 19436);
+ addManagedProfile(admin1, PROFILE_ADMIN, admin1);
+ mContext.binder.callingUid = PROFILE_ADMIN;
+
+ final DevicePolicyManagerInternal dpmi =
+ LocalServices.getService(DevicePolicyManagerInternal.class);
+ assertTrue(dpmi.isActiveSupervisionApp(PROFILE_ADMIN));
+ }
+
// Test if lock timeout on managed profile is handled correctly depending on whether profile
// uses separate challenge.
public void testSetMaximumTimeToLockProfile() throws Exception {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 59d0735..6a818c1 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1729,10 +1729,13 @@
public void registerAppUsageLimitObserver(int observerId, String[] packages,
long timeLimitMs, long timeUsedMs, PendingIntent callbackIntent,
String callingPackage) {
+ final int callingUid = Binder.getCallingUid();
+ final DevicePolicyManagerInternal dpmInternal = getDpmInternal();
if (!hasPermissions(callingPackage,
- Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)) {
- throw new SecurityException("Caller doesn't have both SUSPEND_APPS and "
- + "OBSERVE_APP_USAGE permissions");
+ Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)
+ && (dpmInternal != null && !dpmInternal.isActiveSupervisionApp(callingUid))) {
+ throw new SecurityException("Caller must be the active supervision app or "
+ + "it must have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions");
}
if (packages == null || packages.length == 0) {
@@ -1741,7 +1744,6 @@
if (callbackIntent == null && timeUsedMs < timeLimitMs) {
throw new NullPointerException("callbackIntent can't be null");
}
- final int callingUid = Binder.getCallingUid();
final int userId = UserHandle.getUserId(callingUid);
final long token = Binder.clearCallingIdentity();
try {
@@ -1754,13 +1756,15 @@
@Override
public void unregisterAppUsageLimitObserver(int observerId, String callingPackage) {
+ final int callingUid = Binder.getCallingUid();
+ final DevicePolicyManagerInternal dpmInternal = getDpmInternal();
if (!hasPermissions(callingPackage,
- Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)) {
- throw new SecurityException("Caller doesn't have both SUSPEND_APPS and "
- + "OBSERVE_APP_USAGE permissions");
+ Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)
+ && (dpmInternal != null && !dpmInternal.isActiveSupervisionApp(callingUid))) {
+ throw new SecurityException("Caller must be the active supervision app or "
+ + "it must have both SUSPEND_APPS and OBSERVE_APP_USAGE permissions");
}
- final int callingUid = Binder.getCallingUid();
final int userId = UserHandle.getUserId(callingUid);
final long token = Binder.clearCallingIdentity();
try {