[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"