Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 14 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+2

* Remove AndroidX warning.
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,64 @@
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 BackgroundExecutionContext backgroundExecutionContext;

// Schedule the alarm to be handled by the 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>
*
* <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
* 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");
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());
}
assert (backgroundExecutionContext == null);
Copy link
Contributor

Choose a reason for hiding this comment

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

assertions won't have any effect in release mode. In fact, I'm not even sure Android supports assert() at all. What should the behavior be if this happens in a real app?

backgroundExecutionContext = new BackgroundExecutionContext();
backgroundExecutionContext.startBackgroundIsolate(context, callbackHandle);
}

/**
* Called once the Dart isolate ({@code sBackgroundFlutterView}) has finished initializing.
* Called once the Dart isolate ({@code backgroundFlutterView}) has finished initializing.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is backgroundFlutterView still the correct concept? There shouldn't be any "views" associated with background execution, I don't think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this was an oversight. Thought I had caught all the instances of backgroundFlutterView in the code base... will remove.

*
* <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() {
static void onInitialized() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we have a style pattern of placing some kind of commented identifier before package private members, e.g., /* package */. Would you mind adding one here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I wasn't aware. I'll go ahead and do that.

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);
backgroundExecutionContext.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;
BackgroundExecutionContext.setCallbackDispatcher(context, callbackHandle);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason that this state changing behavior needs to be static rather than per-instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this plugin there's only one valid callback dispatcher, so it doesn't really make sense to make this per-instance state.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if this plugin is being used by 2+ isolates/FlutterEngines? Is there still just 1 dispatcher across multiple isolates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, there's one dispatcher across multiple isolates. All callbacks will execute on the single isolate spawned by this plugin, regardless of which isolate they were scheduled on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Could that cause unintended consequences? @xster

}

public static void setPluginRegistrant(PluginRegistrantCallback callback) {
sPluginRegistrantCallback = callback;
}

/**
* Executes the desired Dart callback in a background Dart isolate.
*
* <p>The given {@code intent} should contain a {@code long} extra called "callbackHandle", which
* corresponds to a callback registered with the Dart VM.
*/
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);
// Indirectly set in BackgroundExecutionContext for backwards compatibility.
BackgroundExecutionContext.setPluginRegistrant(callback);
}

private static void scheduleAlarm(
Expand Down Expand Up @@ -364,7 +231,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 +250,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 +266,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 +316,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 (backgroundExecutionContext == null) {
backgroundExecutionContext = new BackgroundExecutionContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

If we always create a BackgroundExecutionContext in onCreate() then is there any reason to have that instantiation logic elsewhere? And is there any reason to ever think it might be null after this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instantiation in onCreate is to cover the case where the application is woken up in the background via an intent to process an alarm event. In this case, we follow a slightly different initialization path that loads the callback handle from SharedPreferences

}
Context context = getApplicationContext();
backgroundExecutionContext.startBackgroundIsolate(context);
}

/**
Expand All @@ -470,17 +333,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 (!backgroundExecutionContext.isRunning()) {
Log.i(TAG, "AlarmService has not yet started.");
sAlarmQueue.add(intent);
alarmQueue.add(intent);
return;
}
}
Expand All @@ -493,7 +356,7 @@ protected void onHandleWork(final Intent intent) {
new Runnable() {
@Override
public void run() {
executeDartCallbackInBackgroundIsolate(intent, latch);
backgroundExecutionContext.executeDartCallbackInBackgroundIsolate(intent, latch);
}
});

Expand Down
Loading