Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[android_alarm_manager] migrate to the V2 Android embedding #2193

Merged
merged 24 commits into from
Nov 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/android_alarm_manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.4.5

* Add support for Flutter Android embedding V2

## 0.4.4+3

* Add unit tests and DartDocs.
Expand Down
31 changes: 30 additions & 1 deletion packages/android_alarm_manager/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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'
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
* 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}.
*
* <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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that this isn't really setting a View, it's setting a FlutterNativeView, which is now essentially represented by FlutterEngine. Are you sure this method can be deleted? Where is this behavior handled now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an artifact from the original implementation of this plugin when we were running all callbacks on the main isolate, which meant we had to be able to handle the case when the application was moved from foreground to background. This plugin now instead starts a background isolate immediately when AndroidAlarmManager.initialize is invoked or when the AlarmService is started via an Intent, and this isolate is kept alive until the service shuts down.

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(
Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<String> persistentAlarms = prefs.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
if (persistentAlarms == null) {
persistentAlarms = new HashSet<>();
Expand All @@ -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<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) {
return;
Expand All @@ -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<String> persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null);
// No alarms to reschedule.
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -470,17 +360,17 @@ public void onCreate() {
* 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;
}
}
Expand All @@ -493,7 +383,7 @@ protected void onHandleWork(final Intent intent) {
new Runnable() {
@Override
public void run() {
executeDartCallbackInBackgroundIsolate(intent, latch);
flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(intent, latch);
}
});

Expand Down
Loading