diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 1494506fcb73..055086daf6d2 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 d711772c2bb8..52da12479a40 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -23,7 +23,10 @@ apply plugin: 'com.android.library' 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 @@ dependencies { 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 bb3d0c8db102..fb6e7f85b317 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 sAlarmQueue = Collections.synchronizedList(new LinkedList()); + // TODO(mattcarroll): make alarmQueue per-instance, not static. + private static List alarmQueue = Collections.synchronizedList(new LinkedList()); /** Background Dart execution context. */ - private static FlutterNativeView sBackgroundFlutterView; - - /** - * 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; + private static FlutterBackgroundExecutor flutterBackgroundExecutor; - // 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}. - * - *

The isolate is configured as follows: - * - *

    - *
  • Bundle Path: {@code FlutterMain.findAppBundlePath(context)}. - *
  • Entrypoint: The Dart method represented by {@code callbackHandle}. - *
  • Run args: none. - *
+ * Starts the background isolate for the {@link AlarmService}. * *

Preconditions: * *

    *
  • 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. - *
  • A static {@link #sPluginRegistrantCallback} must exist, otherwise a {@link + *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link * PluginRegistrantException} will be thrown. *
*/ 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. * *

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 i = sAlarmQueue.iterator(); + Iterator 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. * - *

The given {@code intent} should contain a {@code long} extra called "callbackHandle", which - * corresponds to a callback registered with the Dart VM. + *

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 @@ private static void scheduleAlarm( } } + /** 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 @@ public static void setOneShot(Context context, AndroidAlarmManagerPlugin.OneShot 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 @@ public static void setPeriodic( 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 @@ private static void addPersistentAlarm( String key = getPersistentAlarmKey(requestCode); SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - synchronized (sPersistentAlarmsLock) { + synchronized (persistentAlarmsLock) { Set persistentAlarms = prefs.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); if (persistentAlarms == null) { persistentAlarms = new HashSet<>(); @@ -383,7 +277,7 @@ private static void addPersistentAlarm( private static void clearPersistentAlarm(Context context, int requestCode) { SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - synchronized (sPersistentAlarmsLock) { + synchronized (persistentAlarmsLock) { Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) { return; @@ -399,7 +293,7 @@ private static void clearPersistentAlarm(Context context, int requestCode) { } public static void reschedulePersistentAlarms(Context context) { - synchronized (sPersistentAlarmsLock) { + synchronized (persistentAlarmsLock) { SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); // No alarms to reschedule. @@ -449,15 +343,11 @@ public static void reschedulePersistentAlarms(Context context) { @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 @@ public void onCreate() { * intent}, then the desired Dart callback is invoked immediately. * *

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 @@ protected void onHandleWork(final Intent intent) { 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 5cc77413928e..9db627022f72 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. * */ -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 @@ public class AndroidAlarmManagerPlugin implements MethodCallHandler, ViewDestroy * 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); + if (instance == null) { + instance = new AndroidAlarmManagerPlugin(); + } + instance.onAttachedToEngine(registrar.context(), registrar.messenger()); + } - // 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); + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + onAttachedToEngine( + binding.getApplicationContext(), binding.getFlutterEngine().getDartExecutor()); + } - // 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); + public void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { + synchronized (initializationLock) { + if (alarmManagerPluginChannel != null) { + return; + } - // 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); + 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); + } } - private Context mContext; - - private AndroidAlarmManagerPlugin(Context context) { - this.mContext = context; + @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 @@ public void onMethodCall(MethodCall call, Result result) { // 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 @@ public void onMethodCall(MethodCall call, Result result) { } } - /** - * Transitions the Flutter execution context that owns this plugin from foreground execution to - * background execution. - * - *

Invoked when the {@link FlutterView} connected to the given {@link FlutterNativeView} is - * destroyed. - * - *

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 000000000000..c7eebc2332bf --- /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. + * + *

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. + * + *

The isolate is configured as follows: + * + *

    + *
  • Bundle Path: {@code FlutterMain.findAppBundlePath(context)}. + *
  • Entrypoint: The Dart method used the last time this plugin was initialized in the + * foreground. + *
  • Run args: none. + *
+ * + *

Preconditions: + * + *

    + *
  • 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. + *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link + * PluginRegistrantException} will be thrown. + *
+ */ + 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}. + * + *

The isolate is configured as follows: + * + *

    + *
  • Bundle Path: {@code FlutterMain.findAppBundlePath(context)}. + *
  • Entrypoint: The Dart method represented by {@code callbackHandle}. + *
  • Run args: none. + *
+ * + *

Preconditions: + * + *

    + *
  • 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. + *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link + * PluginRegistrantException} will be thrown. + *
+ */ + 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. + * + *

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 000000000000..6b69d39de003 --- /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 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 7d87c6e1aae0..c67a8dd8f4f8 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 @@ -9,6 +9,15 @@ android:name=".Application" android:label="android_alarm_manager_example" android:icon="@mipmap/ic_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 000000000000..17a37b8dd156 --- /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 3d9afa5235c3..2c80708c4e94 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 53ae0ae470eb..b6e61b62b903 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 000000000000..e7b4def49cb5 --- /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 5e20364312bb..12aad9b001a9 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -18,6 +18,8 @@ Future main() async { final int periodicID = 0; final int oneShotID = 1; + WidgetsFlutterBinding.ensureInitialized(); + // Start the AlarmManager service. await AndroidAlarmManager.initialize(); @@ -27,7 +29,7 @@ Future main() async { 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 392c03dc8902..3046f16a1405 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -6,8 +6,13 @@ dependencies: 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 000000000000..8359bfd59ef2 --- /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 get _localPath async { + final Directory directory = await getTemporaryDirectory(); + return directory.path; +} + +Future get _localFile async { + final String path = await _localPath; + return File('$path/counter.txt'); +} + +Future writeCounter(int counter) async { + final File file = await _localFile; + + // Write the file. + return file.writeAsString('$counter'); +} + +Future 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 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.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.delayed(const Duration(seconds: 2)); + // poll until file is updated + while (await readCounter() == startingValue) { + await Future.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.delayed(const Duration(seconds: 1)); + } + expect(await readCounter(), startingValue + 2); + expect(await AndroidAlarmManager.cancel(alarmId), isTrue); + await Future.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 000000000000..eea5e8abc15f --- /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> 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 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 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 f31b4bc87525..b8afa134472c 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 @@ 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 +// 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 29a9961b448d..9239fa41b6ef 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 homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager @@ -15,10 +15,12 @@ dev_dependencies: 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"