From c312eeaa5a60420671aa6429537320bff889ee50 Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Wed, 10 Mar 2021 23:59:15 -0800 Subject: [PATCH] All exact alarms now require SCHEDULE_EXACT_ALARM Even exact alarms outside of idle require this permission. This is to draw a clear boundary of the permission. If the app needs exact timing, it should request this permission and then choose the most appropriate API for its needs. Otherwise, it has to work with inexact alarms, which now cannot have a window smaller than a set minimum, currently, ten seconds. Test: atest FrameworksMockingServicesTests:com.android.server.alarm atest CtsAlarmManagerTestCases Bug: 182226633 Change-Id: Iec953dbe8570f1ea6aaf81d864468ef1eb690de6 --- .../java/android/app/AlarmManager.java | 12 + .../server/alarm/AlarmManagerService.java | 81 ++--- .../com/android/server/alarm/AlarmStore.java | 5 + .../server/alarm/BatchingAlarmStore.java | 10 +- .../android/server/alarm/LazyAlarmStore.java | 10 +- core/api/current.txt | 4 +- .../server/alarm/AlarmManagerServiceTest.java | 278 +++++++++++++++--- 7 files changed, 324 insertions(+), 76 deletions(-) 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 61d2db931843e..f936e5afb5563 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4349,8 +4349,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