[android_alarm_manager] migrate to the V2 Android embedding (#2193)
diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md
index 1494506..055086d 100644
--- a/packages/android_alarm_manager/CHANGELOG.md
+++ b/packages/android_alarm_manager/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.4.5
+
+* Add support for Flutter Android embedding V2
+
## 0.4.4+3
* Add unit tests and DartDocs.
diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle
index d711772..52da124 100644
--- a/packages/android_alarm_manager/android/build.gradle
+++ b/packages/android_alarm_manager/android/build.gradle
@@ -23,7 +23,10 @@
android {
compileSdkVersion 28
-
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -37,3 +40,29 @@
implementation 'androidx.appcompat:appcompat:1.0.0'
api 'androidx.core:core:1.0.1'
}
+
+// TODO(bkonyi): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348
+afterEvaluate {
+ def containsEmbeddingDependencies = false
+ for (def configuration : configurations.all) {
+ for (def dependency : configuration.dependencies) {
+ if (dependency.group == 'io.flutter' &&
+ dependency.name.startsWith('flutter_embedding') &&
+ dependency.isTransitive())
+ {
+ containsEmbeddingDependencies = true
+ break
+ }
+ }
+ }
+ if (!containsEmbeddingDependencies) {
+ android {
+ dependencies {
+ def lifecycle_version = "1.1.1"
+ api 'android.arch.lifecycle:runtime:$lifecycle_version'
+ api 'android.arch.lifecycle:common:$lifecycle_version'
+ api 'android.arch.lifecycle:common-java8:$lifecycle_version'
+ }
+ }
+ }
+}
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 bb3d0c8..fb6e7f8 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
@@ -13,12 +13,7 @@
import android.util.Log;
import androidx.core.app.AlarmManagerCompat;
import androidx.core.app.JobIntentService;
-import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
-import io.flutter.view.FlutterCallbackInformation;
-import io.flutter.view.FlutterMain;
-import io.flutter.view.FlutterNativeView;
-import io.flutter.view.FlutterRunArguments;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -27,192 +22,88 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONException;
import org.json.JSONObject;
public class AlarmService extends JobIntentService {
- // TODO(mattcarroll): tags should be private. Make private if no public usage.
- public static final String TAG = "AlarmService";
- private static final String CALLBACK_HANDLE_KEY = "callback_handle";
+ private static final String TAG = "AlarmService";
private static final String PERSISTENT_ALARMS_SET_KEY = "persistent_alarm_ids";
- private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin";
+ protected 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 final Object persistentAlarmsLock = new Object();
- // TODO(mattcarroll): make sIsIsolateRunning per-instance, not static.
- private static AtomicBoolean sIsIsolateRunning = new AtomicBoolean(false);
-
- // TODO(mattcarroll): make sAlarmQueue per-instance, not static.
- private static List<Intent> sAlarmQueue = Collections.synchronizedList(new LinkedList<Intent>());
+ // TODO(mattcarroll): make alarmQueue per-instance, not static.
+ private static List<Intent> alarmQueue = Collections.synchronizedList(new LinkedList<Intent>());
/** Background Dart execution context. */
- private static FlutterNativeView sBackgroundFlutterView;
+ private static FlutterBackgroundExecutor flutterBackgroundExecutor;
- /**
- * The {@link MethodChannel} that connects the Android side of this plugin with the background
- * Dart isolate that was created by this plugin.
- */
- private static MethodChannel sBackgroundChannel;
-
- private static PluginRegistrantCallback sPluginRegistrantCallback;
-
- // Schedule the alarm to be handled by the AlarmService.
+ /** Schedule the alarm to be handled by the {@link AlarmService}. */
public static void enqueueAlarmProcessing(Context context, Intent alarmContext) {
enqueueWork(context, AlarmService.class, JOB_ID, alarmContext);
}
/**
- * Starts running a background Dart isolate within a new {@link FlutterNativeView}.
- *
- * <p>The isolate is configured as follows:
- *
- * <ul>
- * <li>Bundle Path: {@code FlutterMain.findAppBundlePath(context)}.
- * <li>Entrypoint: The Dart method represented by {@code callbackHandle}.
- * <li>Run args: none.
- * </ul>
+ * Starts the background isolate for the {@link AlarmService}.
*
* <p>Preconditions:
*
* <ul>
* <li>The given {@code callbackHandle} must correspond to a registered Dart callback. If the
* handle does not resolve to a Dart callback then this method does nothing.
- * <li>A static {@link #sPluginRegistrantCallback} must exist, otherwise a {@link
+ * <li>A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link
* PluginRegistrantException} will be thrown.
* </ul>
*/
public static void startBackgroundIsolate(Context context, long callbackHandle) {
- // TODO(mattcarroll): re-arrange order of operations. The order is strange - there are 3
- // conditions that must be met for this method to do anything but they're split up for no
- // apparent reason. Do the qualification checks first, then execute the method's logic.
- FlutterMain.ensureInitializationComplete(context, null);
- String mAppBundlePath = FlutterMain.findAppBundlePath(context);
- FlutterCallbackInformation flutterCallback =
- FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
- if (flutterCallback == null) {
- Log.e(TAG, "Fatal: failed to find callback");
+ if (flutterBackgroundExecutor != null) {
+ Log.w(TAG, "Attempted to start a duplicate background isolate. Returning...");
return;
}
-
- // Note that we're passing `true` as the second argument to our
- // FlutterNativeView constructor. This specifies the FlutterNativeView
- // as a background view and does not create a drawing surface.
- sBackgroundFlutterView = new FlutterNativeView(context, true);
- if (mAppBundlePath != null && !sIsIsolateRunning.get()) {
- if (sPluginRegistrantCallback == null) {
- throw new PluginRegistrantException();
- }
- Log.i(TAG, "Starting AlarmService...");
- FlutterRunArguments args = new FlutterRunArguments();
- args.bundlePath = mAppBundlePath;
- args.entrypoint = flutterCallback.callbackName;
- args.libraryPath = flutterCallback.callbackLibraryPath;
- sBackgroundFlutterView.runFromBundle(args);
- sPluginRegistrantCallback.registerWith(sBackgroundFlutterView.getPluginRegistry());
- }
+ flutterBackgroundExecutor = new FlutterBackgroundExecutor();
+ flutterBackgroundExecutor.startBackgroundIsolate(context, callbackHandle);
}
/**
- * Called once the Dart isolate ({@code sBackgroundFlutterView}) has finished initializing.
+ * Called once the Dart isolate ({@code flutterBackgroundExecutor}) has finished initializing.
*
* <p>Invoked by {@link AndroidAlarmManagerPlugin} when it receives the {@code
* AlarmService.initialized} message. Processes all alarm events that came in while the isolate
* was starting.
*/
- // TODO(mattcarroll): consider making this method package private
- public static void onInitialized() {
+ /* package */ static void onInitialized() {
Log.i(TAG, "AlarmService started!");
- sIsIsolateRunning.set(true);
- synchronized (sAlarmQueue) {
+ synchronized (alarmQueue) {
// Handle all the alarm events received before the Dart isolate was
// initialized, then clear the queue.
- Iterator<Intent> i = sAlarmQueue.iterator();
+ Iterator<Intent> i = alarmQueue.iterator();
while (i.hasNext()) {
- executeDartCallbackInBackgroundIsolate(i.next(), null);
+ flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(i.next(), null);
}
- sAlarmQueue.clear();
+ alarmQueue.clear();
}
}
/**
- * Sets the {@link MethodChannel} that is used to communicate with Dart callbacks that are invoked
- * in the background by the android_alarm_manager plugin.
- */
- public static void setBackgroundChannel(MethodChannel channel) {
- sBackgroundChannel = channel;
- }
-
- /**
* Sets the Dart callback handle for the Dart method that is responsible for initializing the
* background Dart isolate, preparing it to receive Dart callback tasks requests.
*/
public static void setCallbackDispatcher(Context context, long callbackHandle) {
- SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
- prefs.edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply();
- }
-
- public static boolean setBackgroundFlutterView(FlutterNativeView view) {
- if (sBackgroundFlutterView != null && sBackgroundFlutterView != view) {
- Log.i(TAG, "setBackgroundFlutterView tried to overwrite an existing FlutterNativeView");
- return false;
- }
- sBackgroundFlutterView = view;
- return true;
- }
-
- public static void setPluginRegistrant(PluginRegistrantCallback callback) {
- sPluginRegistrantCallback = callback;
+ FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle);
}
/**
- * Executes the desired Dart callback in a background Dart isolate.
+ * Sets the {@link PluginRegistrantCallback} used to register the plugins used by an application
+ * with the newly spawned background isolate.
*
- * <p>The given {@code intent} should contain a {@code long} extra called "callbackHandle", which
- * corresponds to a callback registered with the Dart VM.
+ * <p>This should be invoked in {@link Application.onCreate} with {@link
+ * GeneratedPluginRegistrant} in applications using the V1 embedding API in order to use other
+ * plugins in the background isolate. For applications using the V2 embedding API, it is not
+ * necessary to set a {@link PluginRegistrantCallback} as plugins are registered automatically.
*/
- private static void executeDartCallbackInBackgroundIsolate(
- Intent intent, final CountDownLatch latch) {
- // Grab the handle for the callback associated with this alarm. Pay close
- // attention to the type of the callback handle as storing this value in a
- // variable of the wrong size will cause the callback lookup to fail.
- long callbackHandle = intent.getLongExtra("callbackHandle", 0);
- if (sBackgroundChannel == null) {
- Log.e(
- TAG,
- "setBackgroundChannel was not called before alarms were scheduled." + " Bailing out.");
- return;
- }
-
- // If another thread is waiting, then wake that thread when the callback returns a result.
- MethodChannel.Result result = null;
- if (latch != null) {
- result =
- new MethodChannel.Result() {
- @Override
- public void success(Object result) {
- latch.countDown();
- }
-
- @Override
- public void error(String errorCode, String errorMessage, Object errorDetails) {
- latch.countDown();
- }
-
- @Override
- public void notImplemented() {
- latch.countDown();
- }
- };
- }
-
- // Handle the alarm event in Dart. Note that for this plugin, we don't
- // care about the method name as we simply lookup and invoke the callback
- // provided.
- // TODO(mattcarroll): consider giving a method name anyway for the purpose of developer discoverability
- // when reading the source code. Especially on the Dart side.
- sBackgroundChannel.invokeMethod(
- "", new Object[] {callbackHandle, intent.getIntExtra("id", -1)}, result);
+ public static void setPluginRegistrant(PluginRegistrantCallback callback) {
+ // Indirectly set in FlutterBackgroundExecutor for backwards compatibility.
+ FlutterBackgroundExecutor.setPluginRegistrant(callback);
}
private static void scheduleAlarm(
@@ -285,6 +176,7 @@
}
}
+ /** Schedules a one-shot alarm to be executed once in the future. */
public static void setOneShot(Context context, AndroidAlarmManagerPlugin.OneShotRequest request) {
final boolean repeating = false;
scheduleAlarm(
@@ -301,6 +193,7 @@
request.callbackHandle);
}
+ /** Schedules a periodic alarm to be executed repeatedly in the future. */
public static void setPeriodic(
Context context, AndroidAlarmManagerPlugin.PeriodicRequest request) {
final boolean repeating = true;
@@ -320,6 +213,7 @@
request.callbackHandle);
}
+ /** Cancels an alarm with ID {@code requestCode}. */
public static void cancel(Context context, int requestCode) {
// Clear the alarm if it was set to be rescheduled after reboots.
clearPersistentAlarm(context, requestCode);
@@ -364,7 +258,7 @@
String key = getPersistentAlarmKey(requestCode);
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
- synchronized (sPersistentAlarmsLock) {
+ synchronized (persistentAlarmsLock) {
Set<String> persistentAlarms = prefs.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
if (persistentAlarms == null) {
persistentAlarms = new HashSet<>();
@@ -383,7 +277,7 @@
private static void clearPersistentAlarm(Context context, int requestCode) {
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
- synchronized (sPersistentAlarmsLock) {
+ synchronized (persistentAlarmsLock) {
Set<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) {
return;
@@ -399,7 +293,7 @@
}
public static void reschedulePersistentAlarms(Context context) {
- synchronized (sPersistentAlarmsLock) {
+ synchronized (persistentAlarmsLock) {
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
Set<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
// No alarms to reschedule.
@@ -449,15 +343,11 @@
@Override
public void onCreate() {
super.onCreate();
-
- Context context = getApplicationContext();
- FlutterMain.ensureInitializationComplete(context, null);
-
- if (!sIsIsolateRunning.get()) {
- SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
- long callbackHandle = p.getLong(CALLBACK_HANDLE_KEY, 0);
- startBackgroundIsolate(context, callbackHandle);
+ if (flutterBackgroundExecutor == null) {
+ flutterBackgroundExecutor = new FlutterBackgroundExecutor();
}
+ Context context = getApplicationContext();
+ flutterBackgroundExecutor.startBackgroundIsolate(context);
}
/**
@@ -470,17 +360,17 @@
* intent}, then the desired Dart callback is invoked immediately.
*
* <p>If there are any pre-existing callback requests that have yet to be executed, the incoming
- * {@code intent} is added to the {@link #sAlarmQueue} to invoked later, after all pre-existing
+ * {@code intent} is added to the {@link #alarmQueue} to invoked later, after all pre-existing
* callbacks have been executed.
*/
@Override
protected void onHandleWork(final Intent intent) {
// If we're in the middle of processing queued alarms, add the incoming
// intent to the queue and return.
- synchronized (sAlarmQueue) {
- if (!sIsIsolateRunning.get()) {
+ synchronized (alarmQueue) {
+ if (!flutterBackgroundExecutor.isRunning()) {
Log.i(TAG, "AlarmService has not yet started.");
- sAlarmQueue.add(intent);
+ alarmQueue.add(intent);
return;
}
}
@@ -493,7 +383,7 @@
new Runnable() {
@Override
public void run() {
- executeDartCallbackInBackgroundIsolate(intent, latch);
+ flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(intent, latch);
}
});
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 5cc7741..9db6270 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
@@ -5,13 +5,15 @@
package io.flutter.plugins.androidalarmmanager;
import android.content.Context;
+import android.util.Log;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
-import io.flutter.plugin.common.PluginRegistry.ViewDestroyListener;
import io.flutter.view.FlutterNativeView;
import org.json.JSONArray;
import org.json.JSONException;
@@ -38,7 +40,13 @@
* Dart is ready to execute tasks.
* </ol>
*/
-public class AndroidAlarmManagerPlugin implements MethodCallHandler, ViewDestroyListener {
+public class AndroidAlarmManagerPlugin implements FlutterPlugin, MethodCallHandler {
+ private static AndroidAlarmManagerPlugin instance;
+ private final String TAG = "AndroidAlarmManagerPlugin";
+ private Context context;
+ private Object initializationLock = new Object();
+ private MethodChannel alarmManagerPluginChannel;
+
/**
* Registers this plugin with an associated Flutter execution context, represented by the given
* {@link Registrar}.
@@ -47,54 +55,53 @@
* connected to, and running against, the associated Flutter execution context.
*/
public static void registerWith(Registrar registrar) {
- // alarmManagerPluginChannel is the channel responsible for receiving the following messages
- // from the main Flutter app:
- // - "AlarmService.start"
- // - "Alarm.oneShotAt"
- // - "Alarm.periodic"
- // - "Alarm.cancel"
- final MethodChannel alarmManagerPluginChannel =
- new MethodChannel(
- registrar.messenger(),
- "plugins.flutter.io/android_alarm_manager",
- JSONMethodCodec.INSTANCE);
-
- // backgroundCallbackChannel is the channel responsible for receiving the following messages
- // from the background isolate that was setup by this plugin:
- // - "AlarmService.initialized"
- //
- // This channel is also responsible for sending requests from Android to Dart to execute Dart
- // callbacks in the background isolate. Those messages are sent with an empty method name because
- // they are the only messages that this channel sends to Dart.
- final MethodChannel backgroundCallbackChannel =
- new MethodChannel(
- registrar.messenger(),
- "plugins.flutter.io/android_alarm_manager_background",
- JSONMethodCodec.INSTANCE);
-
- // Instantiate a new AndroidAlarmManagerPlugin, connect the primary and background
- // method channels for Android/Flutter communication, and listen for FlutterView
- // destruction so that this plugin can move itself to background mode.
- AndroidAlarmManagerPlugin plugin = new AndroidAlarmManagerPlugin(registrar.context());
- alarmManagerPluginChannel.setMethodCallHandler(plugin);
- backgroundCallbackChannel.setMethodCallHandler(plugin);
- registrar.addViewDestroyListener(plugin);
-
- // The AlarmService expects to hold a static reference to the plugin's background
- // method channel.
- // TODO(mattcarroll): this static reference implies that only one instance of this plugin
- // can exist at a time. Moreover, calling registerWith() a 2nd time would
- // seem to overwrite the previously registered background channel without
- // notice.
- AlarmService.setBackgroundChannel(backgroundCallbackChannel);
+ if (instance == null) {
+ instance = new AndroidAlarmManagerPlugin();
+ }
+ instance.onAttachedToEngine(registrar.context(), registrar.messenger());
}
- private Context mContext;
-
- private AndroidAlarmManagerPlugin(Context context) {
- this.mContext = context;
+ @Override
+ public void onAttachedToEngine(FlutterPluginBinding binding) {
+ onAttachedToEngine(
+ binding.getApplicationContext(), binding.getFlutterEngine().getDartExecutor());
}
+ public void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) {
+ synchronized (initializationLock) {
+ if (alarmManagerPluginChannel != null) {
+ return;
+ }
+
+ Log.i(TAG, "onAttachedToEngine");
+ this.context = applicationContext;
+
+ // alarmManagerPluginChannel is the channel responsible for receiving the following messages
+ // from the main Flutter app:
+ // - "AlarmService.start"
+ // - "Alarm.oneShotAt"
+ // - "Alarm.periodic"
+ // - "Alarm.cancel"
+ alarmManagerPluginChannel =
+ new MethodChannel(
+ messenger, "plugins.flutter.io/android_alarm_manager", JSONMethodCodec.INSTANCE);
+
+ // Instantiate a new AndroidAlarmManagerPlugin and connect the primary method channel for
+ // Android/Flutter communication.
+ alarmManagerPluginChannel.setMethodCallHandler(this);
+ }
+ }
+
+ @Override
+ public void onDetachedFromEngine(FlutterPluginBinding binding) {
+ Log.i(TAG, "onDetachedFromEngine");
+ context = null;
+ alarmManagerPluginChannel.setMethodCallHandler(null);
+ alarmManagerPluginChannel = null;
+ }
+
+ public AndroidAlarmManagerPlugin() {}
+
/** Invoked when the Flutter side of this plugin sends a message to the Android side. */
@Override
public void onMethodCall(MethodCall call, Result result) {
@@ -109,33 +116,26 @@
// method channel to communicate with the new background isolate. Once completed,
// this onMethodCall() method will receive messages from both the primary and background
// method channels.
- AlarmService.setCallbackDispatcher(mContext, callbackHandle);
- AlarmService.startBackgroundIsolate(mContext, callbackHandle);
- result.success(true);
- } else if (method.equals("AlarmService.initialized")) {
- // This message is sent by the background method channel as soon as the background isolate
- // is running. From this point forward, the Android side of this plugin can send
- // callback handles through the background method channel, and the Dart side will execute
- // the Dart methods corresponding to those callback handles.
- AlarmService.onInitialized();
+ AlarmService.setCallbackDispatcher(context, callbackHandle);
+ AlarmService.startBackgroundIsolate(context, callbackHandle);
result.success(true);
} else if (method.equals("Alarm.periodic")) {
// This message indicates that the Flutter app would like to schedule a periodic
// task.
PeriodicRequest periodicRequest = PeriodicRequest.fromJson((JSONArray) arguments);
- AlarmService.setPeriodic(mContext, periodicRequest);
+ AlarmService.setPeriodic(context, periodicRequest);
result.success(true);
} else if (method.equals("Alarm.oneShotAt")) {
// This message indicates that the Flutter app would like to schedule a one-time
// task.
OneShotRequest oneShotRequest = OneShotRequest.fromJson((JSONArray) arguments);
- AlarmService.setOneShot(mContext, oneShotRequest);
+ AlarmService.setOneShot(context, oneShotRequest);
result.success(true);
} else if (method.equals("Alarm.cancel")) {
// This message indicates that the Flutter app would like to cancel a previously
// scheduled task.
int requestCode = ((JSONArray) arguments).getInt(0);
- AlarmService.cancel(mContext, requestCode);
+ AlarmService.cancel(context, requestCode);
result.success(true);
} else {
result.notImplemented();
@@ -147,21 +147,6 @@
}
}
- /**
- * Transitions the Flutter execution context that owns this plugin from foreground execution to
- * background execution.
- *
- * <p>Invoked when the {@link FlutterView} connected to the given {@link FlutterNativeView} is
- * destroyed.
- *
- * <p>Returns true if the given {@code nativeView} was successfully stored by this plugin, or
- * false if a different {@link FlutterNativeView} was already registered with this plugin.
- */
- @Override
- public boolean onViewDestroy(FlutterNativeView nativeView) {
- return AlarmService.setBackgroundFlutterView(nativeView);
- }
-
/** A request to schedule a one-shot Dart task. */
static final class OneShotRequest {
static OneShotRequest fromJson(JSONArray json) throws JSONException {
diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java
new file mode 100644
index 0000000..c7eebc2
--- /dev/null
+++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java
@@ -0,0 +1,236 @@
+// 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.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.AssetManager;
+import android.util.Log;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.dart.DartExecutor.DartCallback;
+import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
+import io.flutter.view.FlutterCallbackInformation;
+import io.flutter.view.FlutterMain;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An background execution abstraction which handles initializing a background isolate running a
+ * callback dispatcher, used to invoke Dart callbacks while backgrounded.
+ */
+public class FlutterBackgroundExecutor implements MethodCallHandler {
+ private static final String TAG = "FlutterBackgroundExecutor";
+ private static final String CALLBACK_HANDLE_KEY = "callback_handle";
+ private static PluginRegistrantCallback pluginRegistrantCallback;
+
+ /**
+ * The {@link MethodChannel} that connects the Android side of this plugin with the background
+ * Dart isolate that was created by this plugin.
+ */
+ private MethodChannel backgroundChannel;
+
+ private FlutterEngine backgroundFlutterEngine;
+
+ private AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false);
+
+ /**
+ * Sets the {@code PluginRegistrantCallback} used to register plugins with the newly spawned
+ * isolate.
+ *
+ * <p>Note: this is only necessary for applications using the V1 engine embedding API as plugins
+ * are automatically registered via reflection in the V2 engine embedding API. If not set, alarm
+ * callbacks will not be able to utilize functionality from other plugins.
+ */
+ public static void setPluginRegistrant(PluginRegistrantCallback callback) {
+ pluginRegistrantCallback = callback;
+ }
+
+ /**
+ * Sets the Dart callback handle for the Dart method that is responsible for initializing the
+ * background Dart isolate, preparing it to receive Dart callback tasks requests.
+ */
+ public static void setCallbackDispatcher(Context context, long callbackHandle) {
+ SharedPreferences prefs = context.getSharedPreferences(AlarmService.SHARED_PREFERENCES_KEY, 0);
+ prefs.edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply();
+ }
+
+ /** Returns true when the background isolate has started and is ready to handle alarms. */
+ public boolean isRunning() {
+ return isCallbackDispatcherReady.get();
+ }
+
+ private void onInitialized() {
+ isCallbackDispatcherReady.set(true);
+ AlarmService.onInitialized();
+ }
+
+ @Override
+ public void onMethodCall(MethodCall call, Result result) {
+ String method = call.method;
+ Object arguments = call.arguments;
+ try {
+ if (method.equals("AlarmService.initialized")) {
+ // This message is sent by the background method channel as soon as the background isolate
+ // is running. From this point forward, the Android side of this plugin can send
+ // callback handles through the background method channel, and the Dart side will execute
+ // the Dart methods corresponding to those callback handles.
+ onInitialized();
+ result.success(true);
+ } else {
+ result.notImplemented();
+ }
+ } catch (PluginRegistrantException e) {
+ result.error("error", "AlarmManager error: " + e.getMessage(), null);
+ }
+ }
+
+ /**
+ * Starts running a background Dart isolate within a new {@link FlutterEngine} using a previously
+ * used entrypoint.
+ *
+ * <p>The isolate is configured as follows:
+ *
+ * <ul>
+ * <li>Bundle Path: {@code FlutterMain.findAppBundlePath(context)}.
+ * <li>Entrypoint: The Dart method used the last time this plugin was initialized in the
+ * foreground.
+ * <li>Run args: none.
+ * </ul>
+ *
+ * <p>Preconditions:
+ *
+ * <ul>
+ * <li>The given callback must correspond to a registered Dart callback. If the handle does not
+ * resolve to a Dart callback then this method does nothing.
+ * <li>A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link
+ * PluginRegistrantException} will be thrown.
+ * </ul>
+ */
+ public void startBackgroundIsolate(Context context) {
+ if (!isRunning()) {
+ SharedPreferences p = context.getSharedPreferences(AlarmService.SHARED_PREFERENCES_KEY, 0);
+ long callbackHandle = p.getLong(CALLBACK_HANDLE_KEY, 0);
+ startBackgroundIsolate(context, callbackHandle);
+ }
+ }
+
+ /**
+ * Starts running a background Dart isolate within a new {@link FlutterEngine}.
+ *
+ * <p>The isolate is configured as follows:
+ *
+ * <ul>
+ * <li>Bundle Path: {@code FlutterMain.findAppBundlePath(context)}.
+ * <li>Entrypoint: The Dart method represented by {@code callbackHandle}.
+ * <li>Run args: none.
+ * </ul>
+ *
+ * <p>Preconditions:
+ *
+ * <ul>
+ * <li>The given {@code callbackHandle} must correspond to a registered Dart callback. If the
+ * handle does not resolve to a Dart callback then this method does nothing.
+ * <li>A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link
+ * PluginRegistrantException} will be thrown.
+ * </ul>
+ */
+ public void startBackgroundIsolate(Context context, long callbackHandle) {
+ if (backgroundFlutterEngine != null) {
+ Log.e(TAG, "Background isolate already started");
+ return;
+ }
+
+ FlutterCallbackInformation flutterCallback =
+ FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
+ if (flutterCallback == null) {
+ Log.e(TAG, "Fatal: failed to find callback");
+ return;
+ }
+ Log.i(TAG, "Starting AlarmService...");
+ String appBundlePath = FlutterMain.findAppBundlePath(context);
+ AssetManager assets = context.getAssets();
+ if (appBundlePath != null && !isRunning()) {
+ backgroundFlutterEngine = new FlutterEngine(context);
+ DartExecutor executor = backgroundFlutterEngine.getDartExecutor();
+ initializeMethodChannel(executor);
+ DartCallback dartCallback = new DartCallback(assets, appBundlePath, flutterCallback);
+
+ executor.executeDartCallback(dartCallback);
+
+ // The pluginRegistrantCallback should only be set in the V1 embedding as
+ // plugin registration is done via reflection in the V2 embedding.
+ if (pluginRegistrantCallback != null) {
+ pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine));
+ }
+ }
+ }
+
+ /**
+ * Executes the desired Dart callback in a background Dart isolate.
+ *
+ * <p>The given {@code intent} should contain a {@code long} extra called "callbackHandle", which
+ * corresponds to a callback registered with the Dart VM.
+ */
+ public void executeDartCallbackInBackgroundIsolate(Intent intent, final CountDownLatch latch) {
+ // Grab the handle for the callback associated with this alarm. Pay close
+ // attention to the type of the callback handle as storing this value in a
+ // variable of the wrong size will cause the callback lookup to fail.
+ long callbackHandle = intent.getLongExtra("callbackHandle", 0);
+
+ // If another thread is waiting, then wake that thread when the callback returns a result.
+ MethodChannel.Result result = null;
+ if (latch != null) {
+ result =
+ new MethodChannel.Result() {
+ @Override
+ public void success(Object result) {
+ latch.countDown();
+ }
+
+ @Override
+ public void error(String errorCode, String errorMessage, Object errorDetails) {
+ latch.countDown();
+ }
+
+ @Override
+ public void notImplemented() {
+ latch.countDown();
+ }
+ };
+ }
+
+ // Handle the alarm event in Dart. Note that for this plugin, we don't
+ // care about the method name as we simply lookup and invoke the callback
+ // provided.
+ backgroundChannel.invokeMethod(
+ "invokeAlarmManagerCallback",
+ new Object[] {callbackHandle, intent.getIntExtra("id", -1)},
+ result);
+ }
+
+ private void initializeMethodChannel(BinaryMessenger isolate) {
+ // backgroundChannel is the channel responsible for receiving the following messages from
+ // the background isolate that was setup by this plugin:
+ // - "AlarmService.initialized"
+ //
+ // This channel is also responsible for sending requests from Android to Dart to execute Dart
+ // callbacks in the background isolate.
+ backgroundChannel =
+ new MethodChannel(
+ isolate,
+ "plugins.flutter.io/android_alarm_manager_background",
+ JSONMethodCodec.INSTANCE);
+ backgroundChannel.setMethodCallHandler(this);
+ }
+}
diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java
new file mode 100644
index 0000000..6b69d39
--- /dev/null
+++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java
@@ -0,0 +1,15 @@
+// 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.androidalarmmanagerexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class MainActivityTest {
+ @Rule public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
+}
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 7d87c6e..c67a8dd 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
@@ -10,15 +10,21 @@
android:label="android_alarm_manager_example"
android:icon="@mipmap/ic_launcher">
<activity
+ android:name="io.flutter.plugins.androidalarmmanagerexample.EmbeddingV1Activity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+ </activity>
+ <activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
- <meta-data
- android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
- android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
diff --git a/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/EmbeddingV1Activity.java b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/EmbeddingV1Activity.java
new file mode 100644
index 0000000..17a37b8
--- /dev/null
+++ b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/EmbeddingV1Activity.java
@@ -0,0 +1,19 @@
+// Copyright 2017 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.androidalarmmanagerexample;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class EmbeddingV1Activity extends FlutterActivity {
+ public static final String TAG = "AlarmExampleMainActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ GeneratedPluginRegistrant.registerWith(this);
+ }
+}
diff --git a/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java
index 3d9afa5..2c80708 100644
--- a/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java
+++ b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java
@@ -1,15 +1,24 @@
+// 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.androidalarmmanagerexample;
-import android.os.Bundle;
-import io.flutter.app.FlutterActivity;
-import io.flutter.plugins.GeneratedPluginRegistrant;
+import dev.flutter.plugins.e2e.E2EPlugin;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
+import io.flutter.plugins.androidalarmmanager.AndroidAlarmManagerPlugin;
+import io.flutter.plugins.pathprovider.PathProviderPlugin;
public class MainActivity extends FlutterActivity {
- public static final String TAG = "AlarmExampleMainActivity";
-
+ // TODO(bkonyi): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694
@Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- GeneratedPluginRegistrant.registerWith(this);
+ public void configureFlutterEngine(FlutterEngine flutterEngine) {
+ ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
+ flutterEngine.getPlugins().add(new AndroidAlarmManagerPlugin());
+ flutterEngine.getPlugins().add(new E2EPlugin());
+ PathProviderPlugin.registerWith(
+ shimPluginRegistry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
}
}
diff --git a/packages/android_alarm_manager/example/android/gradle.properties b/packages/android_alarm_manager/example/android/gradle.properties
index 53ae0ae..b6e61b6 100644
--- a/packages/android_alarm_manager/example/android/gradle.properties
+++ b/packages/android_alarm_manager/example/android/gradle.properties
@@ -1,3 +1,4 @@
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
diff --git a/packages/android_alarm_manager/example/android/settings_aar.gradle b/packages/android_alarm_manager/example/android/settings_aar.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/packages/android_alarm_manager/example/android/settings_aar.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart
index 5e20364..12aad9b 100644
--- a/packages/android_alarm_manager/example/lib/main.dart
+++ b/packages/android_alarm_manager/example/lib/main.dart
@@ -18,6 +18,8 @@
final int periodicID = 0;
final int oneShotID = 1;
+ WidgetsFlutterBinding.ensureInitialized();
+
// Start the AlarmManager service.
await AndroidAlarmManager.initialize();
@@ -27,7 +29,7 @@
Text('See device log for output', textDirection: TextDirection.ltr)));
await AndroidAlarmManager.periodic(
const Duration(seconds: 5), periodicID, printPeriodic,
- wakeup: true);
+ wakeup: true, exact: true);
await AndroidAlarmManager.oneShot(
const Duration(seconds: 5), oneShotID, printOneShot);
}
diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml
index 392c03d..3046f16 100644
--- a/packages/android_alarm_manager/example/pubspec.yaml
+++ b/packages/android_alarm_manager/example/pubspec.yaml
@@ -6,8 +6,13 @@
sdk: flutter
android_alarm_manager:
path: ../
+ e2e: ^0.2.1
+ path_provider: ^1.3.1
+
dev_dependencies:
+ flutter_driver:
+ sdk: flutter
flutter_test:
sdk: flutter
diff --git a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart
new file mode 100644
index 0000000..8359bfd
--- /dev/null
+++ b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart
@@ -0,0 +1,101 @@
+// Copyright 2019, the Chromium project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'package:android_alarm_manager/android_alarm_manager.dart';
+import 'package:e2e/e2e.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path_provider/path_provider.dart';
+
+// From https://flutter.dev/docs/cookbook/persistence/reading-writing-files
+Future<String> get _localPath async {
+ final Directory directory = await getTemporaryDirectory();
+ return directory.path;
+}
+
+Future<File> get _localFile async {
+ final String path = await _localPath;
+ return File('$path/counter.txt');
+}
+
+Future<File> writeCounter(int counter) async {
+ final File file = await _localFile;
+
+ // Write the file.
+ return file.writeAsString('$counter');
+}
+
+Future<int> readCounter() async {
+ try {
+ final File file = await _localFile;
+
+ // Read the file.
+ final String contents = await file.readAsString();
+
+ return int.parse(contents);
+ // ignore: unused_catch_clause
+ } on FileSystemException catch (e) {
+ // If encountering an error, return 0.
+ return 0;
+ }
+}
+
+Future<void> incrementCounter() async {
+ final int value = await readCounter();
+ print('incrementCounter to: ${value + 1}');
+ await writeCounter(value + 1);
+}
+
+void main() {
+ E2EWidgetsFlutterBinding.ensureInitialized();
+
+ print('main');
+ setUp(() async {
+ await AndroidAlarmManager.initialize();
+ });
+
+ group('oneshot', () {
+ testWidgets('cancelled before it fires', (WidgetTester tester) async {
+ final int alarmId = 0;
+ final int startingValue = await readCounter();
+ await AndroidAlarmManager.oneShot(
+ const Duration(seconds: 1), alarmId, incrementCounter);
+ expect(await AndroidAlarmManager.cancel(alarmId), isTrue);
+ await Future<void>.delayed(const Duration(seconds: 4));
+ expect(await readCounter(), startingValue);
+ });
+
+ testWidgets('cancelled after it fires', (WidgetTester tester) async {
+ final int alarmId = 1;
+ final int startingValue = await readCounter();
+ await AndroidAlarmManager.oneShot(
+ const Duration(seconds: 1), alarmId, incrementCounter,
+ exact: true, wakeup: true);
+ await Future<void>.delayed(const Duration(seconds: 2));
+ // poll until file is updated
+ while (await readCounter() == startingValue) {
+ await Future<void>.delayed(const Duration(seconds: 1));
+ }
+ expect(await readCounter(), startingValue + 1);
+ expect(await AndroidAlarmManager.cancel(alarmId), isTrue);
+ });
+ });
+
+ testWidgets('periodic', (WidgetTester tester) async {
+ final int alarmId = 2;
+ final int startingValue = await readCounter();
+ await AndroidAlarmManager.periodic(
+ const Duration(seconds: 1), alarmId, incrementCounter,
+ wakeup: true, exact: true);
+ // poll until file is updated
+ while (await readCounter() < startingValue + 2) {
+ await Future<void>.delayed(const Duration(seconds: 1));
+ }
+ expect(await readCounter(), startingValue + 2);
+ expect(await AndroidAlarmManager.cancel(alarmId), isTrue);
+ await Future<void>.delayed(const Duration(seconds: 3));
+ expect(await readCounter(), startingValue + 2);
+ });
+}
diff --git a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart
new file mode 100644
index 0000000..eea5e8a
--- /dev/null
+++ b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart
@@ -0,0 +1,41 @@
+// Copyright 2019, the Chromium project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:vm_service_client/vm_service_client.dart';
+
+Future<StreamSubscription<VMIsolateRef>> resumeIsolatesOnPause(
+ FlutterDriver driver) async {
+ final VM vm = await driver.serviceClient.getVM();
+ for (VMIsolateRef isolateRef in vm.isolates) {
+ final VMIsolate isolate = await isolateRef.load();
+ if (isolate.isPaused) {
+ await isolate.resume();
+ }
+ }
+ return driver.serviceClient.onIsolateRunnable
+ .asBroadcastStream()
+ .listen((VMIsolateRef isolateRef) async {
+ final VMIsolate isolate = await isolateRef.load();
+ if (isolate.isPaused) {
+ await isolate.resume();
+ }
+ });
+}
+
+Future<void> main() async {
+ final FlutterDriver driver = await FlutterDriver.connect();
+ // flutter drive causes isolates to be paused on spawn. The background isolate
+ // for this plugin will need to be resumed for the test to pass.
+ final StreamSubscription<VMIsolateRef> subscription =
+ await resumeIsolatesOnPause(driver);
+ final String result =
+ await driver.requestData(null, timeout: const Duration(minutes: 5));
+ await driver.close();
+ await subscription.cancel();
+ exit(result == 'pass' ? 0 : 1);
+}
diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart
index f31b4bc..b8afa13 100644
--- a/packages/android_alarm_manager/lib/android_alarm_manager.dart
+++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart
@@ -13,17 +13,16 @@
'plugins.flutter.io/android_alarm_manager_background';
// This is the entrypoint for the background isolate. Since we can only enter
-// an isolate once, we setup a MethodChannel to listen for method invokations
+// an isolate once, we setup a MethodChannel to listen for method invocations
// from the native portion of the plugin. This allows for the plugin to perform
// any necessary processing in Dart (e.g., populating a custom object) before
// invoking the provided callback.
void _alarmManagerCallbackDispatcher() {
- const MethodChannel _channel =
- MethodChannel(_backgroundName, JSONMethodCodec());
-
- // Setup Flutter state needed for MethodChannels.
+ // Initialize state necessary for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
+ const MethodChannel _channel =
+ MethodChannel(_backgroundName, JSONMethodCodec());
// This is where the magic happens and we handle background events from the
// native portion of the plugin.
_channel.setMethodCallHandler((MethodCall call) async {
diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml
index 29a9961..9239fa4 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.4+3
+version: 0.4.5
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager
@@ -15,10 +15,12 @@
flutter:
plugin:
- androidPackage: io.flutter.plugins.androidalarmmanager
- pluginClass: AndroidAlarmManagerPlugin
- iosPrefix: FLT
+ platforms:
+ android:
+ package: io.flutter.plugins.androidalarmmanager
+ pluginClass: AndroidAlarmManagerPlugin
environment:
sdk: ">=2.0.0-dev.28.0 <3.0.0"
+ # TODO(bkonyi): set minimum Flutter version to next stable release
flutter: ">=1.2.0 <2.0.0"