Deny SCHEDULE_EXACT_ALARM by default to newer apps
Apps targeting T will not get SCHEDULE_EXACT_ALARM permission by
default, except in a few privileged special cases.
Test: atest FrameworksMockingServicesTests:AlarmManagerServiceTest
atest CtsAlarmManagerTestCases:ExactAlarmsTest
Bug: 226439802
Change-Id: Ie81a9c3bd8b863c3fd64aaf413209ff8862de74b
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 61424ae..1b9cf26 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -27,6 +27,7 @@
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -280,6 +281,18 @@
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long ENABLE_USE_EXACT_ALARM = 218533173L;
+ /**
+ * For apps targeting {@link Build.VERSION_CODES#TIRAMISU} or above, the permission
+ * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the user explicitly
+ * allows it from Settings.
+ *
+ * TODO (b/226439802): change to EnabledSince(T) after SDK finalization.
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2)
+ public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L;
+
@UnsupportedAppUsage
private final IAlarmManager mService;
private final Context mContext;
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index f67e8d2..881453f 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -66,6 +66,7 @@
import android.app.IAlarmManager;
import android.app.PendingIntent;
import android.app.compat.CompatChanges;
+import android.app.role.RoleManager;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
@@ -164,6 +165,7 @@
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
@@ -224,6 +226,7 @@
private ActivityManagerInternal mActivityManagerInternal;
private final EconomyManagerInternal mEconomyManagerInternal;
private PackageManagerInternal mPackageManagerInternal;
+ private RoleManager mRoleManager;
private volatile PermissionManagerServiceInternal mLocalPermissionManager;
final Object mLock = new Object();
@@ -562,6 +565,9 @@
@VisibleForTesting
static final String KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED =
"kill_on_schedule_exact_alarm_revoked";
+ @VisibleForTesting
+ static final String KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT =
+ "schedule_exact_alarm_denied_by_default";
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
@@ -606,6 +612,9 @@
private static final boolean DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = true;
+ // TODO(b/226439802): Flip to true.
+ private static final boolean DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = false;
+
// Minimum futurity of a new alarm
public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
@@ -693,6 +702,14 @@
public boolean KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED =
DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED;
+ /**
+ * When this is {@code true}, apps with the change
+ * {@link AlarmManager#SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT} enabled will not get
+ * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} unless the user grants it to them.
+ */
+ public volatile boolean SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT =
+ DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT;
+
public boolean USE_TARE_POLICY = Settings.Global.DEFAULT_ENABLE_TARE == 1;
private long mLastAllowWhileIdleWhitelistDuration = -1;
@@ -876,6 +893,15 @@
KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED,
DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED);
break;
+ case KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT:
+ final boolean oldValue = SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT;
+
+ SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = properties.getBoolean(
+ KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT,
+ DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
+ handleScheduleExactAlarmDeniedByDefaultChange(oldValue,
+ SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
+ break;
default:
if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) {
// The quotas need to be updated in order, so we can't just rely
@@ -946,6 +972,15 @@
}
}
+ private void handleScheduleExactAlarmDeniedByDefaultChange(boolean oldValue,
+ boolean newValue) {
+ if (oldValue == newValue) {
+ return;
+ }
+ mHandler.obtainMessage(AlarmHandler.CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE,
+ newValue).sendToTarget();
+ }
+
private void migrateAlarmsToNewStoreLocked() {
final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore()
: new BatchingAlarmStore();
@@ -1122,6 +1157,9 @@
pw.print(KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED,
KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED);
pw.println();
+ pw.print(KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT,
+ SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
+ pw.println();
pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY);
pw.println();
@@ -1892,11 +1930,10 @@
if (hasUseExactAlarmInternal(packageName, uid)) {
return;
}
-
- final boolean requested = mExactAlarmCandidates.contains(
- UserHandle.getAppId(uid));
- final boolean denyListed =
- mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+ if (!mExactAlarmCandidates.contains(UserHandle.getAppId(uid))) {
+ // Permission not requested, app op doesn't matter.
+ return;
+ }
final int newMode = mAppOps.checkOpNoThrow(
AppOpsManager.OP_SCHEDULE_EXACT_ALARM, uid, packageName);
@@ -1913,11 +1950,24 @@
mLastOpScheduleExactAlarm.setValueAt(index, newMode);
}
}
+ if (oldMode == newMode) {
+ return;
+ }
+ final boolean allowedByDefault =
+ isScheduleExactAlarmAllowedByDefault(packageName, uid);
- final boolean hadPermission = getScheduleExactAlarmState(requested,
- denyListed, oldMode);
- final boolean hasPermission = getScheduleExactAlarmState(requested,
- denyListed, newMode);
+ final boolean hadPermission;
+ if (oldMode != AppOpsManager.MODE_DEFAULT) {
+ hadPermission = (oldMode == AppOpsManager.MODE_ALLOWED);
+ } else {
+ hadPermission = allowedByDefault;
+ }
+ final boolean hasPermission;
+ if (newMode != AppOpsManager.MODE_DEFAULT) {
+ hasPermission = (newMode == AppOpsManager.MODE_ALLOWED);
+ } else {
+ hasPermission = allowedByDefault;
+ }
if (hadPermission && !hasPermission) {
mHandler.obtainMessage(AlarmHandler.REMOVE_EXACT_ALARMS,
@@ -1939,6 +1989,8 @@
LocalServices.getService(AppStandbyInternal.class);
appStandbyInternal.addListener(new AppStandbyTracker());
+ mRoleManager = getContext().getSystemService(RoleManager.class);
+
mMetricsHelper.registerPuller(() -> mAlarmStore);
}
}
@@ -2525,19 +2577,6 @@
}
}
- private static boolean getScheduleExactAlarmState(boolean requested, boolean denyListed,
- int appOpMode) {
- // This does not account for the state of the USE_EXACT_ALARM permission.
- // The caller should do that separately.
- if (!requested) {
- return false;
- }
- if (appOpMode == AppOpsManager.MODE_DEFAULT) {
- return !denyListed;
- }
- return appOpMode == AppOpsManager.MODE_ALLOWED;
- }
-
boolean hasUseExactAlarmInternal(String packageName, int uid) {
return isUseExactAlarmEnabled(packageName, UserHandle.getUserId(uid))
&& (PermissionChecker.checkPermissionForPreflight(getContext(),
@@ -2545,6 +2584,32 @@
packageName) == PermissionChecker.PERMISSION_GRANTED);
}
+ /**
+ * Returns whether SCHEDULE_EXACT_ALARM is allowed by default.
+ */
+ boolean isScheduleExactAlarmAllowedByDefault(String packageName, int uid) {
+ if (isScheduleExactAlarmDeniedByDefault(packageName, UserHandle.getUserId(uid))) {
+
+ // This is essentially like changing the protection level of the permission to
+ // (privileged|signature|role|appop), but have to implement this logic to maintain
+ // compatibility for older apps.
+ if (mPackageManagerInternal.isPlatformSigned(packageName)
+ || mPackageManagerInternal.isUidPrivileged(uid)) {
+ return true;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final List<String> wellbeingHolders = (mRoleManager != null)
+ ? mRoleManager.getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)
+ : Collections.emptyList();
+ return wellbeingHolders.contains(packageName);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ return !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+ }
+
boolean hasScheduleExactAlarmInternal(String packageName, int uid) {
final long start = mStatLogger.getTime();
@@ -2560,7 +2625,7 @@
final int mode = mAppOps.checkOpNoThrow(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, uid,
packageName);
if (mode == AppOpsManager.MODE_DEFAULT) {
- hasPermission = !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+ hasPermission = isScheduleExactAlarmAllowedByDefault(packageName, uid);
} else {
hasPermission = (mode == AppOpsManager.MODE_ALLOWED);
}
@@ -2860,6 +2925,13 @@
packageName, UserHandle.of(userId));
}
+ private boolean isScheduleExactAlarmDeniedByDefault(String packageName, int userId) {
+ return mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT
+ && CompatChanges.isChangeEnabled(
+ AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, packageName,
+ UserHandle.of(userId));
+ }
+
@NeverCompile // Avoid size overhead of debugging code.
void dumpImpl(IndentingPrintWriter pw) {
synchronized (mLock) {
@@ -3769,26 +3841,27 @@
if (!isExactAlarmChangeEnabled(changedPackage, userId)) {
continue;
}
+ if (isScheduleExactAlarmDeniedByDefault(changedPackage, userId)) {
+ continue;
+ }
if (hasUseExactAlarmInternal(changedPackage, uid)) {
continue;
}
+ if (!mExactAlarmCandidates.contains(UserHandle.getAppId(uid))) {
+ // Permission isn't requested, deny list doesn't matter.
+ continue;
+ }
final int appOpMode;
synchronized (mLock) {
appOpMode = mLastOpScheduleExactAlarm.get(uid,
AppOpsManager.opToDefaultMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM));
}
- final boolean requested = mExactAlarmCandidates.contains(UserHandle.getAppId(uid));
-
- // added: true => package was added to the deny list
- // added: false => package was removed from the deny list
- final boolean hadPermission = getScheduleExactAlarmState(requested, !added,
- appOpMode);
- final boolean hasPermission = getScheduleExactAlarmState(requested, added,
- appOpMode);
-
- if (hadPermission == hasPermission) {
+ if (appOpMode != AppOpsManager.MODE_DEFAULT) {
+ // Deny list doesn't matter.
continue;
}
+ // added: true => package was added to the deny list
+ // added: false => package was removed from the deny list
if (added) {
synchronized (mLock) {
removeExactAlarmsOnPermissionRevokedLocked(uid,
@@ -4634,6 +4707,7 @@
public static final int REFRESH_EXACT_ALARM_CANDIDATES = 11;
public static final int TARE_AFFORDABILITY_CHANGED = 12;
public static final int CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE = 13;
+ public static final int CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE = 14;
AlarmHandler() {
super(Looper.myLooper());
@@ -4759,6 +4833,35 @@
}
}
break;
+ case CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE:
+ final boolean defaultDenied = (Boolean) msg.obj;
+
+ final int[] startedUserIds = mActivityManagerInternal.getStartedUserIds();
+ for (int appId : mExactAlarmCandidates) {
+ for (int userId : startedUserIds) {
+ uid = UserHandle.getUid(userId, appId);
+
+ final AndroidPackage packageForUid =
+ mPackageManagerInternal.getPackage(uid);
+ if (packageForUid == null) {
+ continue;
+ }
+ final String pkg = packageForUid.getPackageName();
+ if (defaultDenied) {
+ if (!hasScheduleExactAlarmInternal(pkg, uid)
+ && !hasUseExactAlarmInternal(pkg, uid)) {
+ synchronized (mLock) {
+ removeExactAlarmsOnPermissionRevokedLocked(uid, pkg,
+ true);
+ }
+ }
+ } else if (hasScheduleExactAlarmInternal(pkg, uid)) {
+ sendScheduleExactAlarmPermissionStateChangedBroadcast(pkg,
+ UserHandle.getUserId(uid));
+ }
+ }
+ }
+ break;
default:
// nope, just ignore it
break;
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index f34c506..06f698e 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -1208,6 +1208,11 @@
public abstract SharedUserApi getSharedUserApi(int sharedUserAppId);
/**
+ * Returns if the given uid is privileged or not.
+ */
+ public abstract boolean isUidPrivileged(int uid);
+
+ /**
* Initiates a package state mutation request, returning the current state as known by
* PackageManager. This allows the later commit request to compare the initial values and
* determine if any state was changed or any packages were updated since the whole request
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index 2fe7913..ec6443d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -722,6 +722,11 @@
return snapshot().getSharedUser(sharedUserAppId);
}
+ @Override
+ public boolean isUidPrivileged(int uid) {
+ return snapshot().isUidPrivileged(uid);
+ }
+
@NonNull
@Override
@Deprecated
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index c9523ec..529def3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -27,6 +27,7 @@
import static android.app.AlarmManager.FLAG_WAKE_FROM_IDLE;
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.app.AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT;
import static android.app.AlarmManager.WINDOW_EXACT;
import static android.app.AlarmManager.WINDOW_HEURISTIC;
import static android.app.AppOpsManager.MODE_ALLOWED;
@@ -122,6 +123,7 @@
import android.app.IAlarmManager;
import android.app.PendingIntent;
import android.app.compat.CompatChanges;
+import android.app.role.RoleManager;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ContentResolver;
import android.content.Context;
@@ -184,6 +186,7 @@
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -232,6 +235,8 @@
@Mock
private PackageManagerInternal mPackageManagerInternal;
@Mock
+ private RoleManager mRoleManager;
+ @Mock
private AppStateTrackerImpl mAppStateTracker;
@Mock
private AlarmManagerService.ClockReceiver mClockReceiver;
@@ -457,6 +462,7 @@
when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
when(mMockContext.getSystemService(BatteryManager.class)).thenReturn(mBatteryManager);
+ when(mMockContext.getSystemService(RoleManager.class)).thenReturn(mRoleManager);
registerAppIds(new String[]{TEST_CALLING_PACKAGE},
new Integer[]{UserHandle.getAppId(TEST_CALLING_UID)});
@@ -3191,6 +3197,70 @@
}
@Test
+ public void isScheduleExactAlarmAllowedByDefault() {
+ final String package1 = "priv";
+ final String package2 = "signed";
+ final String package3 = "normal";
+ final String package4 = "wellbeing";
+ final int uid1 = 1294;
+ final int uid2 = 8321;
+ final int uid3 = 3412;
+ final int uid4 = 4591;
+
+ when(mPackageManagerInternal.isUidPrivileged(uid1)).thenReturn(true);
+ when(mPackageManagerInternal.isUidPrivileged(uid2)).thenReturn(false);
+ when(mPackageManagerInternal.isUidPrivileged(uid3)).thenReturn(false);
+ when(mPackageManagerInternal.isUidPrivileged(uid4)).thenReturn(false);
+
+ when(mPackageManagerInternal.isPlatformSigned(package1)).thenReturn(false);
+ when(mPackageManagerInternal.isPlatformSigned(package2)).thenReturn(true);
+ when(mPackageManagerInternal.isPlatformSigned(package3)).thenReturn(false);
+ when(mPackageManagerInternal.isPlatformSigned(package4)).thenReturn(false);
+
+ when(mRoleManager.getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)).thenReturn(
+ Arrays.asList(package4));
+
+ mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true);
+ mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = false;
+ mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
+ package1,
+ package3,
+ });
+
+ // Deny listed packages will be false.
+ assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2));
+ assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4));
+
+ mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, false);
+ mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true;
+ mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
+ package1,
+ package3,
+ });
+
+ // Same as above, deny listed packages will be false.
+ assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2));
+ assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4));
+
+ mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true);
+ mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true;
+ mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
+ package1,
+ package3,
+ });
+
+ // Deny list doesn't matter now, only exemptions should be true.
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2));
+ assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3));
+ assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4));
+ }
+
+ @Test
public void alarmScheduledAtomPushed() {
for (int i = 0; i < 10; i++) {
final PendingIntent pi = getNewMockPendingIntent();