diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index 77146e0d12824..78c5b156bc459 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -539,6 +539,11 @@ public class AlarmManager { * scheduled as exact. Applications are strongly discouraged from using exact * alarms unnecessarily as they reduce the OS's ability to minimize battery use. * + *

+ * Starting with {@link Build.VERSION_CODES#S}, apps require the + * {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use this + * API. + * * @param type type of alarm. * @param triggerAtMillis time in milliseconds that the alarm should go * off, using the appropriate clock (depending on the alarm type). @@ -558,6 +563,7 @@ public class AlarmManager { * @see #RTC * @see #RTC_WAKEUP */ + @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true) public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null, null, null); @@ -571,7 +577,13 @@ public class AlarmManager { * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be * invoked via the specified target Handler, or on the application's main looper * if {@code null} is passed as the {@code targetHandler} parameter. + * + *

+ * Starting with {@link Build.VERSION_CODES#S}, apps require the + * {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use this + * API. */ + @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true) public void setExact(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener, Handler targetHandler) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag, 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 33f6e0651abca..58fc87476f2a0 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -388,6 +388,8 @@ public class AlarmManagerService extends SystemService { @VisibleForTesting static final String KEY_MAX_INTERVAL = "max_interval"; @VisibleForTesting + static final String KEY_MIN_WINDOW = "min_window"; + @VisibleForTesting static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION = "allow_while_idle_whitelist_duration"; @VisibleForTesting @@ -428,11 +430,13 @@ public class AlarmManagerService extends SystemService { @VisibleForTesting static final String KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW = "allow_while_idle_compat_window"; - private static final String KEY_CRASH_NON_CLOCK_APPS = "crash_non_clock_apps"; + @VisibleForTesting + static final String KEY_CRASH_NON_CLOCK_APPS = "crash_non_clock_apps"; private static final long DEFAULT_MIN_FUTURITY = 5 * 1000; private static final long DEFAULT_MIN_INTERVAL = 60 * 1000; private static final long DEFAULT_MAX_INTERVAL = 365 * INTERVAL_DAY; + private static final long DEFAULT_MIN_WINDOW = 10_000; private static final long DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION = 10 * 1000; private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000; private static final int DEFAULT_MAX_ALARMS_PER_UID = 500; @@ -475,6 +479,9 @@ public class AlarmManagerService extends SystemService { // Maximum alarm recurrence interval public long MAX_INTERVAL = DEFAULT_MAX_INTERVAL; + // Minimum window size for inexact alarms + public long MIN_WINDOW = DEFAULT_MIN_WINDOW; + // BroadcastOptions.setTemporaryAppWhitelistDuration() to use for FLAG_ALLOW_WHILE_IDLE. public long ALLOW_WHILE_IDLE_WHITELIST_DURATION = DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION; @@ -575,6 +582,9 @@ public class AlarmManagerService extends SystemService { ALLOW_WHILE_IDLE_QUOTA = 1; } break; + case KEY_MIN_WINDOW: + MIN_WINDOW = properties.getLong(KEY_MIN_WINDOW, DEFAULT_MIN_WINDOW); + break; case KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA: ALLOW_WHILE_IDLE_COMPAT_QUOTA = properties.getInt( KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA, @@ -738,6 +748,11 @@ public class AlarmManagerService extends SystemService { TimeUtils.formatDuration(MAX_INTERVAL, pw); pw.println(); + pw.print(KEY_MIN_WINDOW); + pw.print("="); + TimeUtils.formatDuration(MIN_WINDOW, pw); + pw.println(); + pw.print(KEY_LISTENER_TIMEOUT); pw.print("="); TimeUtils.formatDuration(LISTENER_TIMEOUT, pw); @@ -1642,6 +1657,7 @@ public class AlarmManagerService extends SystemService { // Fix this window in place, so that as time approaches we don't collapse it. windowLength = maxElapsed - triggerElapsed; } else { + windowLength = Math.max(windowLength, mConstants.MIN_WINDOW); maxElapsed = triggerElapsed + windowLength; } synchronized (mLock) { @@ -1981,8 +1997,10 @@ public class AlarmManagerService extends SystemService { * Returns true if the given uid does not require SCHEDULE_EXACT_ALARM to set exact, * allow-while-idle alarms. */ - boolean isExemptFromPermission(int uid) { - return (UserHandle.isSameApp(mSystemUiUid, uid) || mLocalDeviceIdleController == null + boolean isExemptFromExactAlarmPermission(int uid) { + return (UserHandle.isSameApp(mSystemUiUid, uid) + || UserHandle.isCore(uid) + || mLocalDeviceIdleController == null || mLocalDeviceIdleController.isAppOnWhitelist(UserHandle.getAppId(uid))); } @@ -2002,54 +2020,43 @@ public class AlarmManagerService extends SystemService { mAppOps.checkPackage(callingUid, callingPackage); final boolean allowWhileIdle = (flags & FLAG_ALLOW_WHILE_IDLE) != 0; + final boolean exact = (windowLength == AlarmManager.WINDOW_EXACT); + // make sure the caller is allowed to use the requested kind of alarm, and also + // decide what quota and broadcast options to use. Bundle idleOptions = null; - if (alarmClock != null || allowWhileIdle) { - // make sure the caller is allowed to use the requested kind of alarm, and also - // decide what broadcast options to use. + if (exact || allowWhileIdle) { final boolean needsPermission; - boolean lowQuota; + boolean lowerQuota; if (CompatChanges.isChangeEnabled(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION, callingPackage, UserHandle.getUserHandleForUid(callingUid))) { - if (windowLength != AlarmManager.WINDOW_EXACT) { - needsPermission = false; - lowQuota = true; - idleOptions = isExemptFromPermission(callingUid) ? mOptsWithFgs.toBundle() - : mOptsWithoutFgs.toBundle(); - } else if (alarmClock != null) { - needsPermission = true; - lowQuota = false; - idleOptions = mOptsWithFgs.toBundle(); - } else { - needsPermission = true; - lowQuota = false; - idleOptions = mOptsWithFgs.toBundle(); - } + needsPermission = exact; + lowerQuota = !exact; + idleOptions = exact ? mOptsWithFgs.toBundle() : mOptsWithoutFgs.toBundle(); } else { needsPermission = false; - lowQuota = allowWhileIdle; + lowerQuota = allowWhileIdle; idleOptions = allowWhileIdle ? mOptsWithFgs.toBundle() : null; } if (needsPermission && !canScheduleExactAlarms()) { - if (alarmClock == null && isExemptFromPermission(callingUid)) { - // If the app is on the full system allow-list (not except-idle), we still - // allow the alarms, but with a lower quota to keep pre-S compatibility. - lowQuota = true; - } else { + if (alarmClock != null || !isExemptFromExactAlarmPermission(callingUid)) { final String errorMessage = "Caller " + callingPackage + " needs to hold " + Manifest.permission.SCHEDULE_EXACT_ALARM + " to set " - + ((allowWhileIdle) ? "exact, allow-while-idle" : "alarm-clock") - + " alarms."; + + "exact alarms."; if (mConstants.CRASH_NON_CLOCK_APPS) { throw new SecurityException(errorMessage); } else { Slog.wtf(TAG, errorMessage); - idleOptions = mOptsWithoutFgs.toBundle(); - lowQuota = allowWhileIdle; } } + // If the app is on the full system power allow-list (not except-idle), or we're + // in a soft failure mode, we still allow the alarms. + // We give temporary allowlist to allow-while-idle alarms but without FGS + // capability. Note that apps that are in the power allow-list do not need it. + idleOptions = allowWhileIdle ? mOptsWithoutFgs.toBundle() : null; + lowerQuota = allowWhileIdle; } - if (lowQuota) { + if (lowerQuota) { flags &= ~FLAG_ALLOW_WHILE_IDLE; flags |= FLAG_ALLOW_WHILE_IDLE_COMPAT; } @@ -2998,13 +3005,10 @@ public class AlarmManagerService extends SystemService { /** * Called when an app loses {@link Manifest.permission#SCHEDULE_EXACT_ALARM} to remove alarms * that the app is no longer eligible to use. - * TODO (b/179541791): Revisit and write tests once UX is final. + * TODO (b/179541791): Add revocation history to dumpsys. */ void removeExactAlarmsOnPermissionRevokedLocked(int uid, String packageName) { - if (UserHandle.isCore(uid) || uid == mSystemUiUid) { - return; - } - if (isExemptFromPermission(uid)) { + if (isExemptFromExactAlarmPermission(uid)) { return; } if (!CompatChanges.isChangeEnabled( @@ -3015,7 +3019,7 @@ public class AlarmManagerService extends SystemService { final Predicate whichAlarms = a -> (a.uid == uid && a.packageName.equals(packageName) - && ((a.flags & FLAG_ALLOW_WHILE_IDLE) != 0 || a.alarmClock != null)); + && a.windowLength == AlarmManager.WINDOW_EXACT); final ArrayList removed = mAlarmStore.remove(whichAlarms); final boolean didRemove = !removed.isEmpty(); if (didRemove) { @@ -3873,6 +3877,7 @@ public class AlarmManagerService extends SystemService { return alarm.creatorUid; } + @VisibleForTesting class AlarmHandler extends Handler { public static final int ALARM_EVENT = 1; diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java index 0e442d09d5a53..e684b84748b12 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmStore.java @@ -133,6 +133,11 @@ public interface AlarmStore { */ void dumpProto(ProtoOutputStream pos, long nowElapsed); + /** + * @return a name for this alarm store that can be used for debugging and tests. + */ + String getName(); + /** * A functional interface used to update the alarm. Used to describe the update in * {@link #updateAlarmDeliveries(AlarmDeliveryCalculator)} diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java index e7edfb7b56b9f..cb528ba2769a9 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java @@ -27,6 +27,7 @@ import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.StatLogger; import java.text.SimpleDateFormat; @@ -40,6 +41,8 @@ import java.util.function.Predicate; * This keeps the alarms in batches, which are sorted on the start time of their delivery window. */ public class BatchingAlarmStore implements AlarmStore { + @VisibleForTesting + static final String TAG = BatchingAlarmStore.class.getSimpleName(); private final ArrayList mAlarmBatches = new ArrayList<>(); private int mSize; @@ -49,7 +52,7 @@ public class BatchingAlarmStore implements AlarmStore { int REBATCH_ALL_ALARMS = 0; } - final StatLogger mStatLogger = new StatLogger("BatchingAlarmStore stats", new String[]{ + final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{ "REBATCH_ALL_ALARMS", }); @@ -211,6 +214,11 @@ public class BatchingAlarmStore implements AlarmStore { } } + @Override + public String getName() { + return TAG; + } + private void insertAndBatchAlarm(Alarm alarm) { final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1 : attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed()); diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java index 8ca14463a3b56..c37d2c36b0685 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/LazyAlarmStore.java @@ -25,6 +25,7 @@ import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.StatLogger; import java.text.SimpleDateFormat; @@ -38,6 +39,8 @@ import java.util.function.Predicate; * This keeps the alarms in a sorted list, and only batches them at the time of delivery. */ public class LazyAlarmStore implements AlarmStore { + @VisibleForTesting + static final String TAG = LazyAlarmStore.class.getSimpleName(); private final ArrayList mAlarms = new ArrayList<>(); private Runnable mOnAlarmClockRemoved; @@ -47,7 +50,7 @@ public class LazyAlarmStore implements AlarmStore { int GET_NEXT_WAKEUP_DELIVERY_TIME = 1; } - final StatLogger mStatLogger = new StatLogger("LazyAlarmStore stats", new String[]{ + final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{ "GET_NEXT_DELIVERY_TIME", "GET_NEXT_WAKEUP_DELIVERY_TIME", }); @@ -214,4 +217,9 @@ public class LazyAlarmStore implements AlarmStore { a.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARMS, nowElapsed); } } + + @Override + public String getName() { + return TAG; + } } diff --git a/core/api/current.txt b/core/api/current.txt index bca2e92d43c3f..4aa38d64ecf98 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4351,8 +4351,8 @@ package android.app { method public void set(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent); method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent); - method public void setExact(int, long, android.app.PendingIntent); - method public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); + method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, android.app.PendingIntent); + method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent); method public void setInexactRepeating(int, long, long, android.app.PendingIntent); method public void setRepeating(int, long, long, android.app.PendingIntent); 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 f2e85a700327e..28940b34c82a6 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.RTC; import static android.app.AlarmManager.RTC_WAKEUP; import static android.app.AlarmManager.WINDOW_EXACT; import static android.app.AlarmManager.WINDOW_HEURISTIC; +import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; @@ -47,17 +48,20 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.alarm.AlarmManagerService.ACTIVE_INDEX; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.APP_STANDBY_BUCKET_CHANGED; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHARGING_STATUS_CHANGED; +import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_QUOTA; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WINDOW; +import static com.android.server.alarm.AlarmManagerService.Constants.KEY_CRASH_NON_CLOCK_APPS; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LAZY_BATCHING; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MAX_INTERVAL; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUTURITY; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_INTERVAL; +import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_WINDOW; import static com.android.server.alarm.AlarmManagerService.FREQUENT_INDEX; import static com.android.server.alarm.AlarmManagerService.INDEFINITE_DELAY; import static com.android.server.alarm.AlarmManagerService.IS_WAKEUP_MASK; @@ -71,6 +75,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -81,6 +86,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import android.Manifest; import android.app.ActivityManager; @@ -408,7 +414,7 @@ public class AlarmManagerServiceTest { ArgumentCaptor appOpsCallbackCaptor = ArgumentCaptor.forClass( IAppOpsCallback.class); try { - verify(mIAppOpsService).startWatchingMode(eq(AppOpsManager.OP_SCHEDULE_EXACT_ALARM), + verify(mIAppOpsService).startWatchingMode(eq(OP_SCHEDULE_EXACT_ALARM), isNull(), appOpsCallbackCaptor.capture()); } catch (RemoteException e) { // Not expected on a mock. @@ -445,12 +451,12 @@ public class AlarmManagerServiceTest { private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval, int flags, int callingUid) { - setTestAlarm(type, triggerTime, operation, interval, flags, callingUid, null); + setTestAlarm(type, triggerTime, 0, operation, interval, flags, callingUid, null); } - private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval, - int flags, int callingUid, Bundle idleOptions) { - mService.setImpl(type, triggerTime, WINDOW_EXACT, interval, operation, null, "test", flags, + private void setTestAlarm(int type, long triggerTime, long windowLength, + PendingIntent operation, long interval, int flags, int callingUid, Bundle idleOptions) { + mService.setImpl(type, triggerTime, windowLength, interval, operation, null, "test", flags, null, null, callingUid, TEST_CALLING_PACKAGE, idleOptions); } @@ -572,6 +578,7 @@ public class AlarmManagerServiceTest { setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW, 35); setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION, 40); setDeviceConfigLong(KEY_LISTENER_TIMEOUT, 45); + setDeviceConfigLong(KEY_MIN_WINDOW, 50); assertEquals(5, mService.mConstants.MIN_FUTURITY); assertEquals(10, mService.mConstants.MIN_INTERVAL); assertEquals(15, mService.mConstants.MAX_INTERVAL); @@ -581,6 +588,7 @@ public class AlarmManagerServiceTest { assertEquals(35, mService.mConstants.ALLOW_WHILE_IDLE_COMPAT_WINDOW); assertEquals(40, mService.mConstants.ALLOW_WHILE_IDLE_WHITELIST_DURATION); assertEquals(45, mService.mConstants.LISTENER_TIMEOUT); + assertEquals(50, mService.mConstants.MIN_WINDOW); } @Test @@ -1644,6 +1652,10 @@ public class AlarmManagerServiceTest { getNewMockPendingIntent(), null, null, null, mock(AlarmManager.AlarmClockInfo.class)); + // exact + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + 0, getNewMockPendingIntent(), null, null, null, null); + // exact, allow-while-idle mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, getNewMockPendingIntent(), null, null, null, null); @@ -1657,6 +1669,22 @@ public class AlarmManagerServiceTest { verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt()); } + @Test + public void exactBinderCallWhenChangeDisabled() throws Exception { + doReturn(false).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + + final PendingIntent alarmPi = getNewMockPendingIntent(); + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + 0, alarmPi, null, null, null, null); + + verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L), + eq(alarmPi), isNull(), isNull(), + eq(FLAG_STANDALONE), isNull(), isNull(), + eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), isNull()); + } + @Test public void exactAllowWhileIdleBinderCallWhenChangeDisabled() throws Exception { doReturn(false).when( @@ -1745,6 +1773,86 @@ public class AlarmManagerServiceTest { assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type); } + @Test + public void alarmClockBinderCallWithoutPermission() throws RemoteException { + setDeviceConfigBoolean(KEY_CRASH_NON_CLOCK_APPS, true); + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true); + + final PendingIntent alarmPi = getNewMockPendingIntent(); + final AlarmManager.AlarmClockInfo alarmClock = mock(AlarmManager.AlarmClockInfo.class); + try { + mBinder.set(TEST_CALLING_PACKAGE, RTC_WAKEUP, 1234, WINDOW_EXACT, 0, 0, + alarmPi, null, null, null, alarmClock); + fail("alarm clock binder call succeeded without permission"); + } catch (SecurityException se) { + // Expected. + } + + verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt()); + } + + @Test + public void exactBinderCallWithPermission() throws RemoteException { + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + + // Permission check is granted by default by the mock. + final PendingIntent alarmPi = getNewMockPendingIntent(); + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + 0, alarmPi, null, null, null, null); + + verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt()); + + final ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L), + eq(alarmPi), isNull(), isNull(), + eq(FLAG_STANDALONE), isNull(), isNull(), + eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture()); + + final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue()); + final int type = idleOptions.getTemporaryAppAllowlistType(); + assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type); + } + + @Test + public void exactBinderCallWithAllowlist() throws RemoteException { + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + // If permission is denied, only then allowlist will be checked. + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true); + + final PendingIntent alarmPi = getNewMockPendingIntent(); + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + 0, alarmPi, null, null, null, null); + + verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid())); + + final ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L), + eq(alarmPi), isNull(), isNull(), + eq(FLAG_STANDALONE), isNull(), isNull(), + eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture()); + System.out.println("what got captured: " + bundleCaptor.getValue()); + } + @Test public void exactAllowWhileIdleBinderCallWithPermission() throws RemoteException { doReturn(true).when( @@ -1798,48 +1906,57 @@ public class AlarmManagerServiceTest { final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue()); final int type = idleOptions.getTemporaryAppAllowlistType(); - assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type); + // App is on power allowlist, doesn't need explicit FGS grant in broadcast options. + assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, type); } @Test - public void inexactAllowWhileIdleBinderCallWithAllowlist() throws RemoteException { - doReturn(true).when( - () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), - anyString(), any(UserHandle.class))); - - when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true); - final PendingIntent alarmPi = getNewMockPendingIntent(); - mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0, - FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null); - - verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, - Manifest.permission.SCHEDULE_EXACT_ALARM), never()); - verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid())); - - final ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); - verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L), - eq(alarmPi), isNull(), isNull(), eq(FLAG_ALLOW_WHILE_IDLE_COMPAT), isNull(), - isNull(), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture()); - - final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue()); - final int type = idleOptions.getTemporaryAppAllowlistType(); - assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type); - } - - @Test - public void inexactAllowWhileIdleBinderCallWithoutAllowlist() throws RemoteException { + public void exactBinderCallsWithoutPermissionWithoutAllowlist() throws RemoteException { + setDeviceConfigBoolean(KEY_CRASH_NON_CLOCK_APPS, true); doReturn(true).when( () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), anyString(), any(UserHandle.class))); + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(false); + + final PendingIntent alarmPi = getNewMockPendingIntent(); + try { + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + 0, alarmPi, null, null, null, null); + fail("exact binder call succeeded without permission"); + } catch (SecurityException se) { + // Expected. + } + try { + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null); + fail("exact, allow-while-idle binder call succeeded without permission"); + } catch (SecurityException se) { + // Expected. + } + verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM), times(2)); + verify(mDeviceIdleInternal, times(2)).isAppOnWhitelist(anyInt()); + } + + @Test + public void inexactAllowWhileIdleBinderCall() throws RemoteException { + // Both permission and power exemption status don't matter for these alarms. + // We only want to test that the flags and idleOptions are correct. + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + final PendingIntent alarmPi = getNewMockPendingIntent(); mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null); verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, Manifest.permission.SCHEDULE_EXACT_ALARM), never()); - verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid())); + verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt()); final ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L), @@ -1851,6 +1968,97 @@ public class AlarmManagerServiceTest { assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, type); } + @Test + public void binderCallWithUserAllowlist() throws RemoteException { + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext, + Manifest.permission.SCHEDULE_EXACT_ALARM)); + when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true); + when(mAppStateTracker.isUidPowerSaveUserExempt(Process.myUid())).thenReturn(true); + + final PendingIntent alarmPi = getNewMockPendingIntent(); + mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, + FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null); + + final ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L), + eq(alarmPi), isNull(), isNull(), + eq(FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED | FLAG_STANDALONE), isNull(), isNull(), + eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), isNull()); + } + + @Test + public void minWindow() { + final long minWindow = 73; + setDeviceConfigLong(KEY_MIN_WINDOW, minWindow); + + // 0 is WINDOW_EXACT and < 0 is WINDOW_HEURISTIC. + for (int window = 1; window <= minWindow; window++) { + final PendingIntent pi = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME, 0, window, pi, 0, 0, TEST_CALLING_UID, null); + + assertEquals(1, mService.mAlarmStore.size()); + final Alarm a = mService.mAlarmStore.remove(unused -> true).get(0); + assertEquals(minWindow, a.windowLength); + } + } + + @Test + public void opScheduleExactAlarmRevoked() throws Exception { + when(mIAppOpsService.checkOperation(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, + TEST_CALLING_PACKAGE)).thenReturn(AppOpsManager.MODE_ERRORED); + mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE); + assertAndHandleMessageSync(REMOVE_EXACT_ALARMS); + verify(mService).removeExactAlarmsOnPermissionRevokedLocked(TEST_CALLING_UID, + TEST_CALLING_PACKAGE); + } + + @Test + public void removeExactAlarmsOnPermissionRevoked() { + doReturn(true).when( + () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION), + anyString(), any(UserHandle.class))); + + // basic exact alarm + setTestAlarm(ELAPSED_REALTIME, 0, 0, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, + null); + // exact and allow-while-idle alarm + setTestAlarm(ELAPSED_REALTIME, 0, 0, getNewMockPendingIntent(), 0, FLAG_ALLOW_WHILE_IDLE, + TEST_CALLING_UID, null); + // alarm clock + setWakeFromIdle(RTC_WAKEUP, 0, getNewMockPendingIntent()); + + final PendingIntent inexact = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME, 0, 10, inexact, 0, 0, TEST_CALLING_UID, null); + + final PendingIntent inexactAwi = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME, 0, 10, inexactAwi, 0, FLAG_ALLOW_WHILE_IDLE, + TEST_CALLING_UID, null); + + final PendingIntent exactButDifferentUid = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME, 0, 0, exactButDifferentUid, 0, 0, TEST_CALLING_UID + 5, + null); + assertEquals(6, mService.mAlarmStore.size()); + + mService.removeExactAlarmsOnPermissionRevokedLocked(TEST_CALLING_UID, TEST_CALLING_PACKAGE); + + final ArrayList remaining = mService.mAlarmStore.asList(); + assertEquals(3, remaining.size()); + assertTrue("Basic inexact alarm removed", + remaining.removeIf(a -> a.matches(inexact, null))); + assertTrue("Inexact allow-while-idle alarm removed", + remaining.removeIf(a -> a.matches(inexactAwi, null))); + assertTrue("Alarm from different uid removed", + remaining.removeIf(a -> a.matches(exactButDifferentUid, null))); + + // Mock should return false by default. + verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(TEST_CALLING_UID)); + } + @Test public void idleOptionsSentOnExpiration() throws Exception { final long triggerTime = mNowElapsedTest + 5000; @@ -1858,7 +2066,7 @@ public class AlarmManagerServiceTest { final Bundle idleOptions = new Bundle(); idleOptions.putChar("TEST_CHAR_KEY", 'x'); idleOptions.putInt("TEST_INT_KEY", 53); - setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi, 0, 0, TEST_CALLING_UID, + setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, 0, alarmPi, 0, 0, TEST_CALLING_UID, idleOptions); mNowElapsedTest = mTestTimer.getElapsed(); @@ -1885,6 +2093,7 @@ public class AlarmManagerServiceTest { assertTrue(i + "th PendingIntent missing: ", alarmsBefore.removeIf(a -> a.matches(pi, null))); } + assertEquals(BatchingAlarmStore.TAG, mService.mAlarmStore.getName()); setDeviceConfigBoolean(KEY_LAZY_BATCHING, true); @@ -1895,6 +2104,7 @@ public class AlarmManagerServiceTest { assertTrue(i + "th PendingIntent missing: ", alarmsAfter.removeIf(a -> a.matches(pi, null))); } + assertEquals(LazyAlarmStore.TAG, mService.mAlarmStore.getName()); } @After