Skip to content

Commit 07a6f52

Browse files
bkonyiandreidiaconu
authored andcommitted
Added support for persisting alarms across reboots (flutter#1133)
Added support for persisting alarms across reboots. Android doesn't automatically reschedule alarms after a reboot, so alarms set using this plugin would have to be scheduled manually after each reboot. With this change, alarms scheduled with the 'rescheduleOnReboot' flag set to true will be rescheduled at boot.
1 parent c1c59a0 commit 07a6f52

8 files changed

Lines changed: 262 additions & 10 deletions

File tree

packages/android_alarm_manager/CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## 0.4.1
2+
* Added support for setting alarms which persist across reboots.
3+
* Both `AndroidAlarmManager.oneShot` and `AndroidAlarmManager.periodic` have
4+
an optional `rescheduleOnReboot` parameter which specifies whether the new
5+
alarm should be rescheduled to run after a reboot (default: false). If set
6+
to false, the alarm will not survive a device reboot.
7+
* Requires AndroidManifest.xml to be updated to include the following
8+
entries:
9+
10+
```xml
11+
<!--Within the application tag body-->
12+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
13+
14+
<!--Within the manifest tag body-->
15+
<receiver
16+
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
17+
android:enabled="false">
18+
<intent-filter>
19+
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
20+
</intent-filter>
21+
</receiver>
22+
23+
```
24+
125
## 0.4.0
226
* **Breaking change**. Migrated the underlying AlarmService to utilize a
327
BroadcastReceiver with a JobIntentService instead of a Service to handle

packages/android_alarm_manager/README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ Dart code in the background when alarms fire.
88
## Getting Started
99

1010
After importing this plugin to your project as usual, add the following to your
11-
`AndroidManifest.xml`:
11+
`AndroidManifest.xml` within the `<manifest></manifest>` tags:
12+
13+
```xml
14+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
15+
```
16+
17+
Next, within the `<application></application>` tags, add:
1218

1319
```xml
1420
<service
@@ -18,6 +24,14 @@ After importing this plugin to your project as usual, add the following to your
1824
<receiver
1925
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
2026
android:exported="false"/>
27+
<receiver
28+
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
29+
android:enabled="false">
30+
<intent-filter>
31+
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
32+
</intent-filter>
33+
</receiver>
34+
2135
```
2236

2337
Then in Dart code add:

packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@
1818
import io.flutter.view.FlutterNativeView;
1919
import io.flutter.view.FlutterRunArguments;
2020
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
2123
import java.util.Iterator;
2224
import java.util.LinkedList;
2325
import java.util.List;
26+
import java.util.Set;
2427
import java.util.concurrent.atomic.AtomicBoolean;
28+
import org.json.JSONException;
29+
import org.json.JSONObject;
2530

2631
public class AlarmService extends JobIntentService {
2732
public static final String TAG = "AlarmService";
28-
private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin";
2933
private static final String CALLBACK_HANDLE_KEY = "callback_handle";
34+
private static final String PERSISTENT_ALARMS_SET_KEY = "persistent_alarm_ids";
35+
private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin";
3036
private static final int JOB_ID = 1984; // Random job ID.
37+
private static final Object sPersistentAlarmsLock = new Object();
3138
private static AtomicBoolean sStarted = new AtomicBoolean(false);
3239
private static List<Intent> sAlarmQueue = Collections.synchronizedList(new LinkedList<Intent>());
3340
private static FlutterNativeView sBackgroundFlutterView;
@@ -168,7 +175,20 @@ private static void scheduleAlarm(
168175
boolean wakeup,
169176
long startMillis,
170177
long intervalMillis,
178+
boolean rescheduleOnReboot,
171179
long callbackHandle) {
180+
if (rescheduleOnReboot) {
181+
addPersistentAlarm(
182+
context,
183+
requestCode,
184+
repeating,
185+
exact,
186+
wakeup,
187+
startMillis,
188+
intervalMillis,
189+
callbackHandle);
190+
}
191+
172192
// Create an Intent for the alarm and set the desired Dart callback handle.
173193
Intent alarm = new Intent(context, AlarmBroadcastReceiver.class);
174194
alarm.putExtra("callbackHandle", callbackHandle);
@@ -204,9 +224,19 @@ public static void setOneShot(
204224
boolean exact,
205225
boolean wakeup,
206226
long startMillis,
227+
boolean rescheduleOnReboot,
207228
long callbackHandle) {
208229
final boolean repeating = false;
209-
scheduleAlarm(context, requestCode, repeating, exact, wakeup, startMillis, 0, callbackHandle);
230+
scheduleAlarm(
231+
context,
232+
requestCode,
233+
repeating,
234+
exact,
235+
wakeup,
236+
startMillis,
237+
0,
238+
rescheduleOnReboot,
239+
callbackHandle);
210240
}
211241

212242
public static void setPeriodic(
@@ -216,6 +246,7 @@ public static void setPeriodic(
216246
boolean wakeup,
217247
long startMillis,
218248
long intervalMillis,
249+
boolean rescheduleOnReboot,
219250
long callbackHandle) {
220251
final boolean repeating = true;
221252
scheduleAlarm(
@@ -226,10 +257,15 @@ public static void setPeriodic(
226257
wakeup,
227258
startMillis,
228259
intervalMillis,
260+
rescheduleOnReboot,
229261
callbackHandle);
230262
}
231263

232264
public static void cancel(Context context, int requestCode) {
265+
// Clear the alarm if it was set to be rescheduled after reboots.
266+
clearPersistentAlarm(context, requestCode);
267+
268+
// Cancel the alarm with the system alarm service.
233269
Intent alarm = new Intent(context, AlarmBroadcastReceiver.class);
234270
PendingIntent existingIntent =
235271
PendingIntent.getBroadcast(context, requestCode, alarm, PendingIntent.FLAG_NO_CREATE);
@@ -240,4 +276,110 @@ public static void cancel(Context context, int requestCode) {
240276
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
241277
manager.cancel(existingIntent);
242278
}
279+
280+
private static String getPersistentAlarmKey(int requestCode) {
281+
return "android_alarm_manager/persistent_alarm_" + Integer.toString(requestCode);
282+
}
283+
284+
private static void addPersistentAlarm(
285+
Context context,
286+
int requestCode,
287+
boolean repeating,
288+
boolean exact,
289+
boolean wakeup,
290+
long startMillis,
291+
long intervalMillis,
292+
long callbackHandle) {
293+
HashMap<String, Object> alarmSettings = new HashMap<>();
294+
alarmSettings.put("repeating", repeating);
295+
alarmSettings.put("exact", exact);
296+
alarmSettings.put("wakeup", wakeup);
297+
alarmSettings.put("startMillis", startMillis);
298+
alarmSettings.put("intervalMillis", intervalMillis);
299+
alarmSettings.put("callbackHandle", callbackHandle);
300+
JSONObject obj = new JSONObject(alarmSettings);
301+
String key = getPersistentAlarmKey(requestCode);
302+
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
303+
304+
synchronized (sPersistentAlarmsLock) {
305+
Set<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
306+
if (persistentAlarms == null) {
307+
persistentAlarms = new HashSet<>();
308+
}
309+
if (persistentAlarms.isEmpty()) {
310+
RebootBroadcastReceiver.enableRescheduleOnReboot(context);
311+
}
312+
persistentAlarms.add(Integer.toString(requestCode));
313+
p.edit()
314+
.putString(key, obj.toString())
315+
.putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms)
316+
.commit();
317+
}
318+
}
319+
320+
private static void clearPersistentAlarm(Context context, int requestCode) {
321+
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
322+
synchronized (sPersistentAlarmsLock) {
323+
Set<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
324+
if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) {
325+
return;
326+
}
327+
persistentAlarms.remove(requestCode);
328+
String key = getPersistentAlarmKey(requestCode);
329+
p.edit().remove(key).putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms).commit();
330+
331+
if (persistentAlarms.isEmpty()) {
332+
RebootBroadcastReceiver.disableRescheduleOnReboot(context);
333+
}
334+
}
335+
}
336+
337+
public static void reschedulePersistentAlarms(Context context) {
338+
synchronized (sPersistentAlarmsLock) {
339+
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
340+
Set<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
341+
// No alarms to reschedule.
342+
if (persistentAlarms == null) {
343+
return;
344+
}
345+
346+
Iterator<String> it = persistentAlarms.iterator();
347+
while (it.hasNext()) {
348+
int requestCode = Integer.parseInt(it.next());
349+
String key = getPersistentAlarmKey(requestCode);
350+
String json = p.getString(key, null);
351+
if (json == null) {
352+
Log.e(
353+
TAG, "Data for alarm request code " + Integer.toString(requestCode) + " is invalid.");
354+
continue;
355+
}
356+
try {
357+
JSONObject alarm = new JSONObject(json);
358+
boolean repeating = alarm.getBoolean("repeating");
359+
boolean exact = alarm.getBoolean("exact");
360+
boolean wakeup = alarm.getBoolean("wakeup");
361+
long startMillis = alarm.getLong("startMillis");
362+
long intervalMillis = alarm.getLong("intervalMillis");
363+
long callbackHandle = alarm.getLong("callbackHandle");
364+
scheduleAlarm(
365+
context,
366+
requestCode,
367+
repeating,
368+
exact,
369+
wakeup,
370+
startMillis,
371+
intervalMillis,
372+
false,
373+
callbackHandle);
374+
} catch (JSONException e) {
375+
Log.e(
376+
TAG,
377+
"Data for alarm request code "
378+
+ Integer.toString(requestCode)
379+
+ " is invalid: "
380+
+ json);
381+
}
382+
}
383+
}
384+
}
243385
}

packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ private void oneShot(JSONArray arguments) throws JSONException {
8181
boolean exact = arguments.getBoolean(1);
8282
boolean wakeup = arguments.getBoolean(2);
8383
long startMillis = arguments.getLong(3);
84-
long callbackHandle = arguments.getLong(4);
85-
AlarmService.setOneShot(mContext, requestCode, exact, wakeup, startMillis, callbackHandle);
84+
boolean rescheduleOnReboot = arguments.getBoolean(4);
85+
long callbackHandle = arguments.getLong(5);
86+
AlarmService.setOneShot(
87+
mContext, requestCode, exact, wakeup, startMillis, rescheduleOnReboot, callbackHandle);
8688
}
8789

8890
private void periodic(JSONArray arguments) throws JSONException {
@@ -91,9 +93,17 @@ private void periodic(JSONArray arguments) throws JSONException {
9193
boolean wakeup = arguments.getBoolean(2);
9294
long startMillis = arguments.getLong(3);
9395
long intervalMillis = arguments.getLong(4);
94-
long callbackHandle = arguments.getLong(5);
96+
boolean rescheduleOnReboot = arguments.getBoolean(5);
97+
long callbackHandle = arguments.getLong(6);
9598
AlarmService.setPeriodic(
96-
mContext, requestCode, exact, wakeup, startMillis, intervalMillis, callbackHandle);
99+
mContext,
100+
requestCode,
101+
exact,
102+
wakeup,
103+
startMillis,
104+
intervalMillis,
105+
rescheduleOnReboot,
106+
callbackHandle);
97107
}
98108

99109
private void cancel(JSONArray arguments) throws JSONException {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.androidalarmmanager;
6+
7+
import android.content.BroadcastReceiver;
8+
import android.content.ComponentName;
9+
import android.content.Context;
10+
import android.content.Intent;
11+
import android.content.pm.PackageManager;
12+
import android.util.Log;
13+
14+
public class RebootBroadcastReceiver extends BroadcastReceiver {
15+
@Override
16+
public void onReceive(Context context, Intent intent) {
17+
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
18+
Log.i("AlarmService", "Rescheduling after boot!");
19+
AlarmService.reschedulePersistentAlarms(context);
20+
}
21+
}
22+
23+
public static void enableRescheduleOnReboot(Context context) {
24+
scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
25+
}
26+
27+
public static void disableRescheduleOnReboot(Context context) {
28+
scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
29+
}
30+
31+
private static void scheduleOnReboot(Context context, int state) {
32+
ComponentName receiver = new ComponentName(context, RebootBroadcastReceiver.class);
33+
PackageManager pm = context.getPackageManager();
34+
pm.setComponentEnabledSetting(receiver, state, PackageManager.DONT_KILL_APP);
35+
}
36+
}

packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="io.flutter.plugins.androidalarmmanagerexample">
33

4+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
45
<uses-permission android:name="android.permission.INTERNET"/>
56

67
<application
@@ -29,5 +30,12 @@
2930
<receiver
3031
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
3132
android:exported="false"/>
33+
<receiver
34+
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
35+
android:enabled="false">
36+
<intent-filter>
37+
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
38+
</intent-filter>
39+
</receiver>
3240
</application>
3341
</manifest>

0 commit comments

Comments
 (0)