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 d984a927badf..5e2fda1f1d7a 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 @@ public static void setOneShot( 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 @@ public static void setPeriodic( 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 void cancel(Context context, int requestCode) { } 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 @@ public static void setPluginRegistrant(PluginRegistrantCallback callback) { 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); - } - } - - @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; + mAppBundlePath = FlutterMain.findAppBundlePath(context); } + // 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 @@ private static void scheduleAlarm( 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 d27cb5922422..6b5c394053a6 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 @@ public static void registerWith(Registrar registrar) { 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 @@ public void onMethodCall(MethodCall call, Result result) { 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 @@ public void onMethodCall(MethodCall call, Result result) { } } + 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 @@ private void periodic(JSONArray arguments) throws JSONException { 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 @@ private void cancel(JSONArray arguments) throws JSONException { @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 43f2f8a082e6..4e6ac36efd50 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -71,15 +71,20 @@ Future main() async { 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 a1271ede0fb7..23b9876bcd86 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 @@ class AndroidAlarmManager { 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 initialize() async { + final CallbackHandle handle = + PluginUtilities.getCallbackHandle(_alarmManagerCallbackDispatcher); + if (handle == null) { + return false; + } + final dynamic r = await _channel + .invokeMethod('AlarmService.start', [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 @@ class AndroidAlarmManager { }) 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', [id, exact, wakeup, first, functionName]); + final dynamic r = await _channel.invokeMethod('Alarm.oneShot', [ + 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 @@ class AndroidAlarmManager { 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', - [id, exact, wakeup, first, period, functionName]); + [id, exact, wakeup, first, period, handle.toRawHandle()]); return (r == null) ? false : r; } @@ -108,20 +169,4 @@ class AndroidAlarmManager { await _channel.invokeMethod('Alarm.cancel', [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); - } }