Updated `android_alarm_manager` to work after engine refactor. (#642)

* Updated `android_alarm_manager` to work after engine refactor.

Fixes issue #17566: alarm_manager plugin stopped working
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 d984a92..5e2fda1 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
@@ -4,28 +4,71 @@
 
 package io.flutter.plugins.androidalarmmanager;
 
-import android.app.Activity;
 import android.app.AlarmManager;
-import android.app.Application;
 import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
 import android.util.Log;
-import io.flutter.app.FlutterActivity;
-import io.flutter.app.FlutterApplication;
+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.concurrent.atomic.AtomicBoolean;
 
 public class AlarmService extends Service {
   public static final String TAG = "AlarmService";
-  private static FlutterNativeView sSharedFlutterView;
+  private static AtomicBoolean sStarted = new AtomicBoolean(false);
+  private static FlutterNativeView sBackgroundFlutterView;
+  private static MethodChannel sBackgroundChannel;
   private static PluginRegistrantCallback sPluginRegistrantCallback;
 
-  private FlutterNativeView mFlutterView;
-  private String appBundlePath;
+  private String mAppBundlePath;
+
+  public static void onInitialized() {
+    sStarted.set(true);
+  }
+
+  // Here we start the AlarmService. This method does a few things:
+  //   - Retrieves the callback information for the handle associated with the
+  //     callback dispatcher in the Dart portion of the plugin.
+  //   - Builds the arguments object for running in a new FlutterNativeView.
+  //   - Enters the isolate owned by the FlutterNativeView at the callback
+  //     represented by `callbackHandle` and initializes the callback
+  //     dispatcher.
+  //   - Registers the FlutterNativeView's PluginRegistry to receive
+  //     MethodChannel messages.
+  public static void startAlarmService(Context context, long callbackHandle) {
+    FlutterMain.ensureInitializationComplete(context, null);
+    String mAppBundlePath = FlutterMain.findAppBundlePath(context);
+    FlutterCallbackInformation cb =
+        FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
+    if (cb == null) {
+      Log.e(TAG, "Fatal: failed to find callback");
+      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 && !sStarted.get()) {
+      Log.i(TAG, "Starting AlarmService...");
+      FlutterRunArguments args = new FlutterRunArguments();
+      args.bundlePath = mAppBundlePath;
+      args.entrypoint = cb.callbackName;
+      args.libraryPath = cb.callbackLibraryPath;
+      sBackgroundFlutterView.runFromBundle(args);
+      sPluginRegistrantCallback.registerWith(sBackgroundFlutterView.getPluginRegistry());
+    }
+  }
+
+  public static void setBackgroundChannel(MethodChannel channel) {
+    sBackgroundChannel = channel;
+  }
 
   public static void setOneShot(
       Context context,
@@ -33,9 +76,9 @@
       boolean exact,
       boolean wakeup,
       long startMillis,
-      String entrypoint) {
+      long callbackHandle) {
     final boolean repeating = false;
-    scheduleAlarm(context, requestCode, repeating, exact, wakeup, startMillis, 0, entrypoint);
+    scheduleAlarm(context, requestCode, repeating, exact, wakeup, startMillis, 0, callbackHandle);
   }
 
   public static void setPeriodic(
@@ -45,10 +88,17 @@
       boolean wakeup,
       long startMillis,
       long intervalMillis,
-      String entrypoint) {
+      long callbackHandle) {
     final boolean repeating = true;
     scheduleAlarm(
-        context, requestCode, repeating, exact, wakeup, startMillis, intervalMillis, entrypoint);
+        context,
+        requestCode,
+        repeating,
+        exact,
+        wakeup,
+        startMillis,
+        intervalMillis,
+        callbackHandle);
   }
 
   public static void cancel(Context context, int requestCode) {
@@ -64,16 +114,15 @@
   }
 
   public static FlutterNativeView getSharedFlutterView() {
-    return sSharedFlutterView;
+    return sBackgroundFlutterView;
   }
 
-  public static boolean setSharedFlutterView(FlutterNativeView view) {
-    if (sSharedFlutterView != null && sSharedFlutterView != view) {
-      Log.i(TAG, "setSharedFlutterView tried to overwrite an existing FlutterNativeView");
+  public static boolean setBackgroundFlutterView(FlutterNativeView view) {
+    if (sBackgroundFlutterView != null && sBackgroundFlutterView != view) {
+      Log.i(TAG, "setBackgroundFlutterView tried to overwrite an existing FlutterNativeView");
       return false;
     }
-    Log.i(TAG, "setSharedFlutterView set");
-    sSharedFlutterView = view;
+    sBackgroundFlutterView = view;
     return true;
   }
 
@@ -81,92 +130,37 @@
     sPluginRegistrantCallback = callback;
   }
 
-  private void ensureFlutterView() {
-    if (mFlutterView != null) {
-      return;
-    }
-
-    if (sSharedFlutterView != null) {
-      mFlutterView = sSharedFlutterView;
-      return;
-    }
-
-    // mFlutterView and sSharedFlutterView are both null. That likely means that
-    // no FlutterView has ever been created in this process before. So, we'll
-    // make one, and assign it to both mFlutterView and sSharedFlutterView.
-    mFlutterView = new FlutterNativeView(getApplicationContext());
-    sSharedFlutterView = mFlutterView;
-
-    // If there was no FlutterNativeView before now, then we also must
-    // initialize the PluginRegistry.
-    sPluginRegistrantCallback.registerWith(mFlutterView.getPluginRegistry());
-    return;
-  }
-
-  // This returns the FlutterView for the main FlutterActivity if there is one.
-  private static FlutterNativeView viewFromAppContext(Context context) {
-    Application app = (Application) context;
-    if (!(app instanceof FlutterApplication)) {
-      Log.i(TAG, "viewFromAppContext app not a FlutterApplication");
-      return null;
-    }
-    FlutterApplication flutterApp = (FlutterApplication) app;
-    Activity activity = flutterApp.getCurrentActivity();
-    if (activity == null) {
-      Log.i(TAG, "viewFromAppContext activity is null");
-      return null;
-    }
-    if (!(activity instanceof FlutterActivity)) {
-      Log.i(TAG, "viewFromAppContext activity is not a FlutterActivity");
-      return null;
-    }
-    FlutterActivity flutterActivity = (FlutterActivity) activity;
-    return flutterActivity.getFlutterView().getFlutterNativeView();
-  }
-
   @Override
   public void onCreate() {
     super.onCreate();
     Context context = getApplicationContext();
-    mFlutterView = viewFromAppContext(context);
     FlutterMain.ensureInitializationComplete(context, null);
-    if (appBundlePath == null) {
-      appBundlePath = FlutterMain.findAppBundlePath(context);
-    }
+    mAppBundlePath = FlutterMain.findAppBundlePath(context);
   }
 
-  @Override
-  public void onDestroy() {
-    // Try to find the native view of the main activity if there is one.
-    Context context = getApplicationContext();
-    FlutterNativeView nativeView = viewFromAppContext(context);
-
-    // Don't destroy mFlutterView if it is the same as the native view for the
-    // main activity, or the same as the shared native view.
-    if (mFlutterView != nativeView && mFlutterView != sSharedFlutterView) {
-      mFlutterView.destroy();
-    }
-    mFlutterView = null;
-
-    // Don't destroy the shared native view if it is the same native view as
-    // for the main activity.
-    if (sSharedFlutterView != nativeView) {
-      sSharedFlutterView.destroy();
-    }
-    sSharedFlutterView = null;
-  }
-
+  // This is where we handle alarm events before sending them to our callback
+  // dispatcher in Dart.
   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
-    ensureFlutterView();
-    String entrypoint = intent.getStringExtra("entrypoint");
-    if (entrypoint == null) {
-      Log.i(TAG, "onStartCommand got a null entrypoint. Bailing out");
+    if (!sStarted.get()) {
+      Log.i(TAG, "AlarmService has not yet started.");
+      // TODO(bkonyi): queue up alarm events.
       return START_NOT_STICKY;
     }
-    if (appBundlePath != null) {
-      mFlutterView.runFromBundle(appBundlePath, null, entrypoint, true);
+    // 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 START_NOT_STICKY;
     }
+    // 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.
+    sBackgroundChannel.invokeMethod("", new Object[] {callbackHandle});
     return START_NOT_STICKY;
   }
 
@@ -183,10 +177,10 @@
       boolean wakeup,
       long startMillis,
       long intervalMillis,
-      String entrypoint) {
-    // Create an Intent for the alarm and set the desired Dart entrypoint.
+      long callbackHandle) {
+    // Create an Intent for the alarm and set the desired Dart callback handle.
     Intent alarm = new Intent(context, AlarmService.class);
-    alarm.putExtra("entrypoint", entrypoint);
+    alarm.putExtra("callbackHandle", callbackHandle);
     PendingIntent pendingIntent =
         PendingIntent.getService(context, requestCode, alarm, PendingIntent.FLAG_UPDATE_CURRENT);
 
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 d27cb59..6b5c394 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
@@ -25,9 +25,16 @@
             registrar.messenger(),
             "plugins.flutter.io/android_alarm_manager",
             JSONMethodCodec.INSTANCE);
+    final MethodChannel backgroundChannel =
+        new MethodChannel(
+            registrar.messenger(),
+            "plugins.flutter.io/android_alarm_manager_background",
+            JSONMethodCodec.INSTANCE);
     AndroidAlarmManagerPlugin plugin = new AndroidAlarmManagerPlugin(registrar.context());
     channel.setMethodCallHandler(plugin);
+    backgroundChannel.setMethodCallHandler(plugin);
     registrar.addViewDestroyListener(plugin);
+    AlarmService.setBackgroundChannel(backgroundChannel);
   }
 
   private Context mContext;
@@ -41,7 +48,12 @@
     String method = call.method;
     Object arguments = call.arguments;
     try {
-      if (method.equals("Alarm.periodic")) {
+      if (method.equals("AlarmService.start")) {
+        startService((JSONArray) arguments);
+        result.success(true);
+      } else if (method.equals("AlarmService.initialized")) {
+        AlarmService.onInitialized();
+      } else if (method.equals("Alarm.periodic")) {
         periodic((JSONArray) arguments);
         result.success(true);
       } else if (method.equals("Alarm.oneShot")) {
@@ -58,13 +70,18 @@
     }
   }
 
+  private void startService(JSONArray arguments) throws JSONException {
+    long callbackHandle = arguments.getLong(0);
+    AlarmService.startAlarmService(mContext, callbackHandle);
+  }
+
   private void oneShot(JSONArray arguments) throws JSONException {
     int requestCode = arguments.getInt(0);
     boolean exact = arguments.getBoolean(1);
     boolean wakeup = arguments.getBoolean(2);
     long startMillis = arguments.getLong(3);
-    String entrypoint = arguments.getString(4);
-    AlarmService.setOneShot(mContext, requestCode, exact, wakeup, startMillis, entrypoint);
+    long callbackHandle = arguments.getLong(4);
+    AlarmService.setOneShot(mContext, requestCode, exact, wakeup, startMillis, callbackHandle);
   }
 
   private void periodic(JSONArray arguments) throws JSONException {
@@ -73,9 +90,9 @@
     boolean wakeup = arguments.getBoolean(2);
     long startMillis = arguments.getLong(3);
     long intervalMillis = arguments.getLong(4);
-    String entrypoint = arguments.getString(5);
+    long callbackHandle = arguments.getLong(5);
     AlarmService.setPeriodic(
-        mContext, requestCode, exact, wakeup, startMillis, intervalMillis, entrypoint);
+        mContext, requestCode, exact, wakeup, startMillis, intervalMillis, callbackHandle);
   }
 
   private void cancel(JSONArray arguments) throws JSONException {
@@ -85,6 +102,6 @@
 
   @Override
   public boolean onViewDestroy(FlutterNativeView nativeView) {
-    return AlarmService.setSharedFlutterView(nativeView);
+    return AlarmService.setBackgroundFlutterView(nativeView);
   }
 }
diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart
index 43f2f8a..4e6ac36 100644
--- a/packages/android_alarm_manager/example/lib/main.dart
+++ b/packages/android_alarm_manager/example/lib/main.dart
@@ -71,15 +71,20 @@
   final int helloAlarmID = 0;
   final int goodbyeAlarmID = 1;
   final int oneShotID = 2;
+
+  // Start the AlarmManager service.
+  await AndroidAlarmManager.initialize();
+
   printHelloMessage("Hello, main()!");
   runApp(const Center(
       child: Text('Hello, world!', textDirection: TextDirection.ltr)));
   await AndroidAlarmManager.periodic(
-      const Duration(minutes: 1), helloAlarmID, printHello);
-  await AndroidAlarmManager.periodic(
-      const Duration(minutes: 1), goodbyeAlarmID, printGoodbye);
+      const Duration(seconds: 5), helloAlarmID, printHello,
+      wakeup: true);
+  await AndroidAlarmManager.oneShot(
+      const Duration(seconds: 5), goodbyeAlarmID, printGoodbye);
   if (!oneShotFired) {
     await AndroidAlarmManager.oneShot(
-        const Duration(minutes: 1), oneShotID, printOneShot);
+        const Duration(seconds: 5), oneShotID, printOneShot);
   }
 }
diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart
index a1271ed..23b9876 100644
--- a/packages/android_alarm_manager/lib/android_alarm_manager.dart
+++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart
@@ -3,9 +3,49 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
+import 'dart:ui';
 
+import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
+const String _backgroundName =
+    '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
+// 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.
+  WidgetsFlutterBinding.ensureInitialized();
+
+  // This is where the magic happens and we handle background events from the
+  // native portion of the plugin.
+  _channel.setMethodCallHandler((MethodCall call) async {
+    final dynamic args = call.arguments;
+    final CallbackHandle handle = new CallbackHandle.fromRawHandle(args[0]);
+
+    // PluginUtilities.getCallbackFromHandle performs a lookup based on the
+    // callback handle and returns a tear-off of the original callback.
+    final Function closure = PluginUtilities.getCallbackFromHandle(handle);
+
+    if (closure == null) {
+      print('Fatal: could not find callback');
+      exit(-1);
+    }
+    closure();
+  });
+
+  // Once we've finished initializing, let the native portion of the plugin
+  // know that it can start scheduling alarms.
+  _channel.invokeMethod('AlarmService.initialized');
+}
+
 /// A Flutter plugin for registering Dart callbacks with the Android
 /// AlarmManager service.
 ///
@@ -15,14 +55,30 @@
   static const MethodChannel _channel =
       MethodChannel(_channelName, JSONMethodCodec());
 
+  /// Starts the [AndroidAlarmManager] service. This must be called before
+  /// setting any alarms.
+  ///
+  /// Returns a [Future] that resolves to `true` on success and `false` on
+  /// failure.
+  static Future<bool> initialize() async {
+    final CallbackHandle handle =
+        PluginUtilities.getCallbackHandle(_alarmManagerCallbackDispatcher);
+    if (handle == null) {
+      return false;
+    }
+    final dynamic r = await _channel
+        .invokeMethod('AlarmService.start', <dynamic>[handle.toRawHandle()]);
+    return r ?? false;
+  }
+
   /// Schedules a one-shot timer to run `callback` after time `delay`.
   ///
   /// The `callback` will run whether or not the main application is running or
-  /// in the foreground. It will run in the same Isolate as the main application
-  /// if one is available, otherwise a new Isolate will be created.
+  /// in the foreground. It will run in the Isolate owned by the
+  /// AndroidAlarmManager service.
   ///
-  /// `callback` must be a top-level function in the application's root library
-  /// (that is, in the same library as the application's `main()` function).
+  /// `callback` must be either a top-level function or a static method from a
+  /// class.
   ///
   /// The timer is uniquely identified by `id`. Calling this function again
   /// again with the same `id` will cancel and replace the existing timer.
@@ -46,23 +102,28 @@
   }) async {
     final int now = new DateTime.now().millisecondsSinceEpoch;
     final int first = now + delay.inMilliseconds;
-    final String functionName = _nameOfFunction(callback);
-    if (functionName == null) {
+    final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
+    if (handle == null) {
       return false;
     }
-    final dynamic r = await _channel.invokeMethod(
-        'Alarm.oneShot', <dynamic>[id, exact, wakeup, first, functionName]);
+    final dynamic r = await _channel.invokeMethod('Alarm.oneShot', <dynamic>[
+      id,
+      exact,
+      wakeup,
+      first,
+      handle.toRawHandle(),
+    ]);
     return (r == null) ? false : r;
   }
 
   /// Schedules a repeating timer to run `callback` with period `duration`.
   ///
   /// The `callback` will run whether or not the main application is running or
-  /// in the foreground. It will run in the same Isolate as the main application
-  /// if one is available, otherwise a new Isolate will be created.
+  /// in the foreground. It will run in the Isolate owned by the
+  /// AndroidAlarmManager service.
   ///
-  /// `callback` must be a top-level function in the application's root library
-  /// (that is, in the same library as the application's `main()` function).
+  /// `callback` must be either a top-level function or a static method from a
+  /// class.
   ///
   /// The repeating timer is uniquely identified by `id`. Calling this function
   /// again with the same `id` will cancel and replace the existing timer.
@@ -87,12 +148,12 @@
     final int now = new DateTime.now().millisecondsSinceEpoch;
     final int period = duration.inMilliseconds;
     final int first = now + period;
-    final String functionName = _nameOfFunction(callback);
-    if (functionName == null) {
+    final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
+    if (handle == null) {
       return false;
     }
     final dynamic r = await _channel.invokeMethod('Alarm.periodic',
-        <dynamic>[id, exact, wakeup, first, period, functionName]);
+        <dynamic>[id, exact, wakeup, first, period, handle.toRawHandle()]);
     return (r == null) ? false : r;
   }
 
@@ -108,20 +169,4 @@
         await _channel.invokeMethod('Alarm.cancel', <dynamic>[id]);
     return (r == null) ? false : r;
   }
-
-  // Extracts the name of a top-level function from the .toString() of its
-  // closure-ization. The Java side of this plugin accepts the entrypoint into
-  // Dart code as a string. However, the Dart side of this API can't use a
-  // string to specify the entrypoint, otherwise it won't be visited by Dart's
-  // AOT compiler.
-  static String _nameOfFunction(dynamic Function() callback) {
-    final String longName = callback.toString();
-    final int functionIndex = longName.indexOf('Function');
-    if (functionIndex == -1) return null;
-    final int openQuote = longName.indexOf("'", functionIndex + 1);
-    if (openQuote == -1) return null;
-    final int closeQuote = longName.indexOf("'", openQuote + 1);
-    if (closeQuote == -1) return null;
-    return longName.substring(openQuote + 1, closeQuote);
-  }
 }