From 306ed287f45d9dc72228125e59001cf6c940fe0c Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Thu, 6 May 2021 16:27:01 -0700 Subject: [PATCH] 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:; 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 --- .../java/android/app/AlarmManager.java | 31 +++++++++++++++++++ .../android/os/PowerExemptionManager.java | 9 ++++++ .../server/alarm/AlarmManagerService.java | 30 ++++++++++++++++++ core/api/current.txt | 1 + core/res/AndroidManifest.xml | 2 ++ .../server/alarm/AlarmManagerServiceTest.java | 28 +++++++++++++++++ 6 files changed, 101 insertions(+) diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index 01f31e43989ec..b096537821caa 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.NonNull; 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 class AlarmManager { 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. + * + *

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. + * + *

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. + * + *

Note: + * 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. + * + *

This broadcast will be sent to both runtime receivers and manifest receivers. + * + *

This broadcast is sent as a foreground broadcast. + * See {@link android.content.Intent#FLAG_RECEIVER_FOREGROUND}. + * + *

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 b62ece6759c60..9dd1296d681c0 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java @@ -251,6 +251,12 @@ public class PowerExemptionManager { * @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 @@ public class PowerExemptionManager { 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 @@ public class PowerExemptionManager { 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 b3396c5c827c7..0eb260986472a 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.INTERVAL_HOUR; 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 @@ public class AlarmManagerService extends SystemService { 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 @@ public class AlarmManagerService extends SystemService { } } + /** + * 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 de92fd1af6ffe..5f3eeb1ed1911 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4404,6 +4404,7 @@ package android.app { 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 4e7dd913b260f..ffb8d1daac906 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -696,6 +696,8 @@ + + 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 72342810c4b6e..5222511cc479d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -2402,6 +2402,34 @@ public class AlarmManagerServiceTest { TEST_CALLING_PACKAGE); } + @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 intentCaptor = ArgumentCaptor.forClass(Intent.class); + final ArgumentCaptor 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(