Send a broadcast when an app is granted SCHEDULE_EXACT_ALARM
Bug: 187206399
Test: atest AlarmManagerServiceTest
Test: Manual test with a test app.
- Grant the permission to test app and make sure it can start FGS.
Log:
```
05-07 10:12:08.671 1000 1579 9475 I ActivityManager: Background started FGS: Allowed [callingPackage: com.google.omakoto.testapp; callingUid: 10294; uidState: RCVR; intent: Intent { act=fgs cmp=com.google.omakoto.testapp/.MyFgs }; code:(unknown:207); tempAllowListReason:<broadcast:1000:android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED,reason:,reasonCode:(unknown:207),duration:10000,callingUid:1000>; targetSdkVersion:30; callerTargetSdkVersion:30; startForegroundCount:0]
```
- Grant the permission to a different app and make sure the test app
won't receive the broadcast.
Test: CTS -- Incoming
Change-Id: Iefe3e12dcf51318d8433532ba3048caa69b1edcd
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 01f31e4..b096537 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
@@ -138,6 +139,36 @@
public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED =
"android.app.action.NEXT_ALARM_CLOCK_CHANGED";
+ /**
+ * Broadcast Action: An app is granted the
+ * {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM} permission.
+ *
+ * <p>When the user revokes the {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM}
+ * permission, all alarms scheduled with
+ * {@link #setExact}, {@link #setExactAndAllowWhileIdle} and
+ * {@link #setAlarmClock(AlarmClockInfo, PendingIntent)} will be deleted.
+ *
+ * <p>When the user grants the {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM},
+ * this broadcast will be sent. Applications can reschedule all the necessary alarms when
+ * receiving it.
+ *
+ * <p><em>Note:</em>
+ * Applications are still required to check {@link #canScheduleExactAlarms()}
+ * before using the above APIs after receiving this broadcast,
+ * because it's possible that the permission is already revoked again by the time
+ * applications receive this broadcast.
+ *
+ * <p>This broadcast will be sent to both runtime receivers and manifest receivers.
+ *
+ * <p>This broadcast is sent as a foreground broadcast.
+ * See {@link android.content.Intent#FLAG_RECEIVER_FOREGROUND}.
+ *
+ * <p>When an application receives this broadcast, it's allowed to start a foreground service.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED =
+ "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED";
+
/** @hide */
@UnsupportedAppUsage
public static final long WINDOW_EXACT = 0;
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index b62ece6..9dd1296 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -251,6 +251,12 @@
* @hide
*/
public static final int REASON_LOCALE_CHANGED = 206;
+ /**
+ * Broadcast
+ * {@link android.app.AlarmManager#ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED}
+ * @hide
+ */
+ public static final int REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED = 207;
/* Reason code range 300-399 are reserved for other internal reasons */
/**
* Device idle system allow list, including EXCEPT-IDLE
@@ -386,6 +392,7 @@
REASON_TIMEZONE_CHANGED,
REASON_TIME_CHANGED,
REASON_LOCALE_CHANGED,
+ REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED,
REASON_SYSTEM_ALLOW_LISTED,
REASON_ALARM_MANAGER_ALARM_CLOCK,
REASON_ALARM_MANAGER_WHILE_IDLE,
@@ -664,6 +671,8 @@
return "TIME_CHANGED";
case REASON_LOCALE_CHANGED:
return "LOCALE_CHANGED";
+ case REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED:
+ return "REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED";
case REASON_SYSTEM_ALLOW_LISTED:
return "SYSTEM_ALLOW_LISTED";
case REASON_ALARM_MANAGER_ALARM_CLOCK:
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 b3396c5..0eb2609 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -30,6 +30,7 @@
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.RTC_WAKEUP;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.os.PowerExemptionManager.REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED;
import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
import static android.os.PowerWhitelistManager.REASON_ALARM_MANAGER_WHILE_IDLE;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -1704,6 +1705,11 @@
if (!hasScheduleExactAlarmInternal(packageName, uid)) {
mHandler.obtainMessage(AlarmHandler.REMOVE_EXACT_ALARMS,
uid, 0, packageName).sendToTarget();
+ } else {
+ // TODO(b/187206399) Make sure this won't be sent, if the app
+ // already had the appop previously.
+ sendScheduleExactAlarmPermissionStateChangedBroadcast(
+ packageName, UserHandle.getUserId(uid));
}
}
});
@@ -4816,6 +4822,30 @@
}
}
+ /**
+ * Send {@link AlarmManager#ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED} to
+ * the app that is just granted the permission.
+ */
+ private void sendScheduleExactAlarmPermissionStateChangedBroadcast(
+ String packageName, int userId) {
+ final Intent i = new Intent(
+ AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED);
+ i.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_FOREGROUND);
+ i.setPackage(packageName);
+
+ // We need to allow the app to start a foreground service.
+ // This broadcast is very rare, so we do not cache the BroadcastOptions.
+ final BroadcastOptions opts = BroadcastOptions.makeBasic();
+ opts.setTemporaryAppAllowlist(
+ mActivityManagerInternal.getBootTimeTempAllowListDuration(),
+ TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+ REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED, "");
+ getContext().sendBroadcastAsUser(i, UserHandle.of(userId), /*permission*/ null,
+ opts.toBundle());
+ }
+
private void decrementAlarmCount(int uid, int decrement) {
int oldCount = 0;
final int uidIndex = mAlarmsPerUid.indexOfKey(uid);
diff --git a/core/api/current.txt b/core/api/current.txt
index de92fd1..5f3eeb1 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4404,6 +4404,7 @@
method public void setWindow(int, long, long, android.app.PendingIntent);
method public void setWindow(int, long, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
field public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED = "android.app.action.NEXT_ALARM_CLOCK_CHANGED";
+ field public static final String ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED = "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED";
field public static final int ELAPSED_REALTIME = 3; // 0x3
field public static final int ELAPSED_REALTIME_WAKEUP = 2; // 0x2
field public static final long INTERVAL_DAY = 86400000L; // 0x5265c00L
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4e7dd91..ffb8d1d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -696,6 +696,8 @@
<protected-broadcast android:name="android.scheduling.action.REBOOT_READY" />
<protected-broadcast android:name="android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED" />
+ <protected-broadcast android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
+
<!-- ====================================================================== -->
<!-- RUNTIME PERMISSIONS -->
<!-- ====================================================================== -->
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 7234281..5222511 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -2403,6 +2403,34 @@
}
@Test
+ public void opScheduleExactAlarmGranted() throws Exception {
+ final long durationMs = 20000L;
+ when(mActivityManagerInternal.getBootTimeTempAllowListDuration()).thenReturn(durationMs);
+
+ mockExactAlarmPermissionGrant(true, false, MODE_ALLOWED);
+ mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+
+ final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+
+ verify(mMockContext).sendBroadcastAsUser(intentCaptor.capture(), eq(UserHandle.SYSTEM),
+ isNull(), bundleCaptor.capture());
+
+ // Validate the intent.
+ assertEquals(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED,
+ intentCaptor.getValue().getAction());
+ assertEquals(TEST_CALLING_PACKAGE, intentCaptor.getValue().getPackage());
+
+ // Validate the options.
+ final BroadcastOptions bOptions = new BroadcastOptions(bundleCaptor.getValue());
+ assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+ bOptions.getTemporaryAppAllowlistType());
+ assertEquals(durationMs, bOptions.getTemporaryAppAllowlistDuration());
+ assertEquals(PowerExemptionManager.REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED,
+ bOptions.getTemporaryAppAllowlistReasonCode());
+ }
+
+ @Test
public void removeExactAlarmsOnPermissionRevoked() {
doReturn(true).when(
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),