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();