diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index bf58bb8d0a4b..0ec184f085f7 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,27 @@ +## 0.4.1 +* Added support for setting alarms which persist across reboots. + * Both `AndroidAlarmManager.oneShot` and `AndroidAlarmManager.periodic` have + an optional `rescheduleOnReboot` parameter which specifies whether the new + alarm should be rescheduled to run after a reboot (default: false). If set + to false, the alarm will not survive a device reboot. + * Requires AndroidManifest.xml to be updated to include the following + entries: + + ```xml + + + + + + + + + + + ``` + ## 0.4.0 * **Breaking change**. Migrated the underlying AlarmService to utilize a BroadcastReceiver with a JobIntentService instead of a Service to handle diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index bcbb007aac8d..33aeacd5adae 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -8,7 +8,13 @@ Dart code in the background when alarms fire. ## Getting Started After importing this plugin to your project as usual, add the following to your -`AndroidManifest.xml`: +`AndroidManifest.xml` within the `` tags: + +```xml + +``` + +Next, within the `` tags, add: ```xml + + + + + + ``` Then in Dart code add: diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java index 85637cf2e4dd..6d1d3a7b6086 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java @@ -18,16 +18,23 @@ import io.flutter.view.FlutterNativeView; import io.flutter.view.FlutterRunArguments; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import org.json.JSONException; +import org.json.JSONObject; public class AlarmService extends JobIntentService { public static final String TAG = "AlarmService"; - private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin"; private static final String CALLBACK_HANDLE_KEY = "callback_handle"; + private static final String PERSISTENT_ALARMS_SET_KEY = "persistent_alarm_ids"; + private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin"; private static final int JOB_ID = 1984; // Random job ID. + private static final Object sPersistentAlarmsLock = new Object(); private static AtomicBoolean sStarted = new AtomicBoolean(false); private static List sAlarmQueue = Collections.synchronizedList(new LinkedList()); private static FlutterNativeView sBackgroundFlutterView; @@ -168,7 +175,20 @@ private static void scheduleAlarm( boolean wakeup, long startMillis, long intervalMillis, + boolean rescheduleOnReboot, long callbackHandle) { + if (rescheduleOnReboot) { + addPersistentAlarm( + context, + requestCode, + repeating, + exact, + wakeup, + startMillis, + intervalMillis, + callbackHandle); + } + // Create an Intent for the alarm and set the desired Dart callback handle. Intent alarm = new Intent(context, AlarmBroadcastReceiver.class); alarm.putExtra("callbackHandle", callbackHandle); @@ -204,9 +224,19 @@ public static void setOneShot( boolean exact, boolean wakeup, long startMillis, + boolean rescheduleOnReboot, long callbackHandle) { final boolean repeating = false; - scheduleAlarm(context, requestCode, repeating, exact, wakeup, startMillis, 0, callbackHandle); + scheduleAlarm( + context, + requestCode, + repeating, + exact, + wakeup, + startMillis, + 0, + rescheduleOnReboot, + callbackHandle); } public static void setPeriodic( @@ -216,6 +246,7 @@ public static void setPeriodic( boolean wakeup, long startMillis, long intervalMillis, + boolean rescheduleOnReboot, long callbackHandle) { final boolean repeating = true; scheduleAlarm( @@ -226,10 +257,15 @@ public static void setPeriodic( wakeup, startMillis, intervalMillis, + rescheduleOnReboot, callbackHandle); } public static void cancel(Context context, int requestCode) { + // Clear the alarm if it was set to be rescheduled after reboots. + clearPersistentAlarm(context, requestCode); + + // Cancel the alarm with the system alarm service. Intent alarm = new Intent(context, AlarmBroadcastReceiver.class); PendingIntent existingIntent = PendingIntent.getBroadcast(context, requestCode, alarm, PendingIntent.FLAG_NO_CREATE); @@ -240,4 +276,110 @@ public static void cancel(Context context, int requestCode) { AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.cancel(existingIntent); } + + private static String getPersistentAlarmKey(int requestCode) { + return "android_alarm_manager/persistent_alarm_" + Integer.toString(requestCode); + } + + private static void addPersistentAlarm( + Context context, + int requestCode, + boolean repeating, + boolean exact, + boolean wakeup, + long startMillis, + long intervalMillis, + long callbackHandle) { + HashMap alarmSettings = new HashMap<>(); + alarmSettings.put("repeating", repeating); + alarmSettings.put("exact", exact); + alarmSettings.put("wakeup", wakeup); + alarmSettings.put("startMillis", startMillis); + alarmSettings.put("intervalMillis", intervalMillis); + alarmSettings.put("callbackHandle", callbackHandle); + JSONObject obj = new JSONObject(alarmSettings); + String key = getPersistentAlarmKey(requestCode); + SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); + + synchronized (sPersistentAlarmsLock) { + Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); + if (persistentAlarms == null) { + persistentAlarms = new HashSet<>(); + } + if (persistentAlarms.isEmpty()) { + RebootBroadcastReceiver.enableRescheduleOnReboot(context); + } + persistentAlarms.add(Integer.toString(requestCode)); + p.edit() + .putString(key, obj.toString()) + .putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms) + .commit(); + } + } + + private static void clearPersistentAlarm(Context context, int requestCode) { + SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); + synchronized (sPersistentAlarmsLock) { + Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); + if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) { + return; + } + persistentAlarms.remove(requestCode); + String key = getPersistentAlarmKey(requestCode); + p.edit().remove(key).putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms).commit(); + + if (persistentAlarms.isEmpty()) { + RebootBroadcastReceiver.disableRescheduleOnReboot(context); + } + } + } + + public static void reschedulePersistentAlarms(Context context) { + synchronized (sPersistentAlarmsLock) { + SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); + Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); + // No alarms to reschedule. + if (persistentAlarms == null) { + return; + } + + Iterator it = persistentAlarms.iterator(); + while (it.hasNext()) { + int requestCode = Integer.parseInt(it.next()); + String key = getPersistentAlarmKey(requestCode); + String json = p.getString(key, null); + if (json == null) { + Log.e( + TAG, "Data for alarm request code " + Integer.toString(requestCode) + " is invalid."); + continue; + } + try { + JSONObject alarm = new JSONObject(json); + boolean repeating = alarm.getBoolean("repeating"); + boolean exact = alarm.getBoolean("exact"); + boolean wakeup = alarm.getBoolean("wakeup"); + long startMillis = alarm.getLong("startMillis"); + long intervalMillis = alarm.getLong("intervalMillis"); + long callbackHandle = alarm.getLong("callbackHandle"); + scheduleAlarm( + context, + requestCode, + repeating, + exact, + wakeup, + startMillis, + intervalMillis, + false, + callbackHandle); + } catch (JSONException e) { + Log.e( + TAG, + "Data for alarm request code " + + Integer.toString(requestCode) + + " is invalid: " + + json); + } + } + } + } } diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java index b0060c146950..594ac35908c7 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java @@ -81,8 +81,10 @@ private void oneShot(JSONArray arguments) throws JSONException { boolean exact = arguments.getBoolean(1); boolean wakeup = arguments.getBoolean(2); long startMillis = arguments.getLong(3); - long callbackHandle = arguments.getLong(4); - AlarmService.setOneShot(mContext, requestCode, exact, wakeup, startMillis, callbackHandle); + boolean rescheduleOnReboot = arguments.getBoolean(4); + long callbackHandle = arguments.getLong(5); + AlarmService.setOneShot( + mContext, requestCode, exact, wakeup, startMillis, rescheduleOnReboot, callbackHandle); } private void periodic(JSONArray arguments) throws JSONException { @@ -91,9 +93,17 @@ private void periodic(JSONArray arguments) throws JSONException { boolean wakeup = arguments.getBoolean(2); long startMillis = arguments.getLong(3); long intervalMillis = arguments.getLong(4); - long callbackHandle = arguments.getLong(5); + boolean rescheduleOnReboot = arguments.getBoolean(5); + long callbackHandle = arguments.getLong(6); AlarmService.setPeriodic( - mContext, requestCode, exact, wakeup, startMillis, intervalMillis, callbackHandle); + mContext, + requestCode, + exact, + wakeup, + startMillis, + intervalMillis, + rescheduleOnReboot, + callbackHandle); } private void cancel(JSONArray arguments) throws JSONException { diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.java new file mode 100644 index 000000000000..91836ccbdaad --- /dev/null +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.java @@ -0,0 +1,36 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.androidalarmmanager; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.util.Log; + +public class RebootBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) { + Log.i("AlarmService", "Rescheduling after boot!"); + AlarmService.reschedulePersistentAlarms(context); + } + } + + public static void enableRescheduleOnReboot(Context context) { + scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + } + + public static void disableRescheduleOnReboot(Context context) { + scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_DISABLED); + } + + private static void scheduleOnReboot(Context context, int state) { + ComponentName receiver = new ComponentName(context, RebootBroadcastReceiver.class); + PackageManager pm = context.getPackageManager(); + pm.setComponentEnabledSetting(receiver, state, PackageManager.DONT_KILL_APP); + } +} diff --git a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml index e1724dac19ad..862e20924a0d 100644 --- a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart index 36aba4e250b7..9354a1b34840 100644 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart @@ -97,6 +97,10 @@ class AndroidAlarmManager { /// alarm fires. If `wakeup` is false (the default), the device will not be /// woken up to service the alarm. /// + /// If `rescheduleOnReboot` is passed as `true`, the alarm will be persisted + /// across reboots. If `rescheduleOnReboot` is false (the default), the alarm + /// will not be rescheduled after a reboot and will not be executed. + /// /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future oneShot( @@ -105,6 +109,7 @@ class AndroidAlarmManager { dynamic Function() callback, { bool exact = false, bool wakeup = false, + bool rescheduleOnReboot = false, }) async { final int now = DateTime.now().millisecondsSinceEpoch; final int first = now + delay.inMilliseconds; @@ -120,6 +125,7 @@ class AndroidAlarmManager { exact, wakeup, first, + rescheduleOnReboot, handle.toRawHandle(), ]); return (r == null) ? false : r; @@ -145,6 +151,10 @@ class AndroidAlarmManager { /// alarm fires. If `wakeup` is false (the default), the device will not be /// woken up to service the alarm. /// + /// If `rescheduleOnReboot` is passed as `true`, the alarm will be persisted + /// across reboots. If `rescheduleOnReboot` is false (the default), the alarm + /// will not be rescheduled after a reboot and will not be executed. + /// /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future periodic( @@ -153,6 +163,7 @@ class AndroidAlarmManager { dynamic Function() callback, { bool exact = false, bool wakeup = false, + bool rescheduleOnReboot = false, }) async { final int now = DateTime.now().millisecondsSinceEpoch; final int period = duration.inMilliseconds; @@ -164,8 +175,15 @@ class AndroidAlarmManager { // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - final dynamic r = await _channel.invokeMethod('Alarm.periodic', - [id, exact, wakeup, first, period, handle.toRawHandle()]); + final dynamic r = await _channel.invokeMethod('Alarm.periodic', [ + id, + exact, + wakeup, + first, + period, + rescheduleOnReboot, + handle.toRawHandle() + ]); return (r == null) ? false : r; } diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index b12ab8c69563..5d3006a85aef 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.4.0 +version: 0.4.1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager