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

Conversation

bkonyi
Copy link
Contributor

@bkonyi bkonyi commented Oct 15, 2019

Description

Minimum work to migrate to the Android v2 embedding.

Related Issues

flutter/flutter#41830

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (flutter analyze) does not report any problems on my PR.
  • I read and followed the Flutter Style Guide.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy.
  • I updated CHANGELOG.md to add a description of the change.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (please indicate a breaking change in CHANGELOG.md and increment major revision).
  • No, this is not a breaking change.

@bkonyi
Copy link
Contributor Author

bkonyi commented Oct 15, 2019

@matthew-carroll is there anything else here that I missed that's specific to background execution? Also, do we still think onViewDestroyed is necessary or is there a new equivalent for FlutterEngine?

@collinjackson collinjackson self-requested a review October 15, 2019 19:32
Copy link
Contributor

@collinjackson collinjackson left a comment

Choose a reason for hiding this comment

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

Right now this plugin has no unit tests or integration tests. I think that this android_alarm_manager should be straightforward to integration test using the e2e plugin. Would you be open to adding that?

I wrote some example tests that I think should work as a starting point. Feel free to copy these into the PR.

https://github.com/collinjackson/plugins/tree/android_alarm_manager_test/packages/android_alarm_manager/example/test_driver

Note: these tests do not pass for me when I run them under flutter drive (the alarm manager doesn't seem to trigger), and I'm not sure why. Maybe the alarm manager only works when the app is in the background?

final AndroidAlarmManagerPlugin plugin = new AndroidAlarmManagerPlugin();
// Listen for FlutterView destruction so that this plugin can move itself
// to background mode.
registrar.addViewDestroyListener(plugin);
Copy link
Contributor

Choose a reason for hiding this comment

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

@bkonyi do you know what the analogous behavior of this plugin is when using the new embedding? I assume that what this plugin is doing when the view is destroyed is important, right? If so, we won't be able to act on view destruction, but plugins will now have the opportunity to take action when the activity disappears, which is probably what this view destruction really signified in the first place...

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, looking at this code now I'm uncertain as to what it even accomplishes. It appears that on view destruction we try and store the view in AlarmService... but we also create a view in AlarmService.startBackgroundIsolate. I think we're safe to remove this... I'll try and see if it causes any issues.

Copy link
Contributor

@mklim mklim left a comment

Choose a reason for hiding this comment

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

Looking good. It just needs tests like @collinjackson pointed out.

@@ -1,3 +1,9 @@
## 0.4.5
* Add support for Flutter Android embedding V2
* Called WidgetsFlutterBinding.ensureInitialized() in
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this hurts, but in general we've decided that this is the responsibility of the plugin user, not author.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. I can remove this from the initialize method, but is there any reason why we're leaving this up to the plugin user?

final AndroidAlarmManagerPlugin plugin = new AndroidAlarmManagerPlugin();
// Listen for FlutterView destruction so that this plugin can move itself
// to background mode.
registrar.addViewDestroyListener(plugin);
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@bkonyi
Copy link
Contributor Author

bkonyi commented Oct 17, 2019

Right now this plugin has no unit tests or integration tests. I think that this android_alarm_manager should be straightforward to integration test using the e2e plugin. Would you be open to adding that?

I wrote some example tests that I think should work as a starting point. Feel free to copy these into the PR.

https://github.com/collinjackson/plugins/tree/android_alarm_manager_test/packages/android_alarm_manager/example/test_driver

I'm definitely open to adding tests if we can show they're not going to be extremely flaky. The AlarmManager alarms aren't as predictable as I'd like, but I'll see what I can do.

Note: these tests do not pass for me when I run them under flutter drive (the alarm manager doesn't seem to trigger), and I'm not sure why. Maybe the alarm manager only works when the app is in the background?

These tests run on an actual device, correct? There might be an issue with the phone being in doze mode which results in background events being delayed until the device is in a more "active" state to conserve battery life.

@bkonyi
Copy link
Contributor Author

bkonyi commented Oct 17, 2019

@collinjackson it looks like driver tests don't work correctly when additional isolates are spawned (see flutter/flutter#24703). It looks like isolates are being paused on start and the driver only resumes the main isolate, regardless of how the test is launched (both flutter drive and the Gradle e2e) so the background isolate for the alarm manager never actually runs.

@@ -152,15 +152,6 @@ public static void setCallbackDispatcher(Context context, long callbackHandle) {
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.

@collinjackson
Copy link
Contributor

collinjackson commented Oct 17, 2019

@collinjackson it looks like driver tests don't work correctly when additional isolates are spawned (see flutter/flutter#24703). It looks like isolates are being paused on start and the driver only resumes the main isolate, regardless of how the test is launched (both flutter drive and the Gradle e2e) so the background isolate for the alarm manager never actually runs.

The Gradle e2e method doesn't use Flutter driver, so I'm surprised it doesn't work. I wonder if @vishna's workaround in flutter/flutter#24703 (comment) would be helpful here.

@bkonyi
Copy link
Contributor Author

bkonyi commented Oct 18, 2019

@collinjackson it looks like driver tests don't work correctly when additional isolates are spawned (see flutter/flutter#24703). It looks like isolates are being paused on start and the driver only resumes the main isolate, regardless of how the test is launched (both flutter drive and the Gradle e2e) so the background isolate for the alarm manager never actually runs.

The Gradle e2e method doesn't use Flutter driver, so I'm surprised it doesn't work. I wonder if @vishna's workaround in flutter/flutter#24703 (comment) would be helpful here.

Disregard, for some reason the background callback dispatcher wasn't calling WidgetsFlutterBinding.ensureInitialized so the isolate was started but was unable to communicate that back to the plugin. Also, that workaround didn't seem to work before, but I'll give it another shot to see if I can get this working with flutter drive as well.

However, I am still running into some issues with plugins seemingly not being recognized when running tests. It looks like the alarms are firing correctly but we're failing to write the count to the temporary file. We were swallowing the exception before, but I'm seeing this failure:

MissingPluginException(No implementation found for method getTemporaryDirectory on channel plugins.flutter.io/path_provider)

I'm also seeing this warning in the logs: Warning: E2E test plugin was not detected.

I've tried cleaning the project and re-running and haven't had any luck. The GeneratedPluginRegistrant looks like it's setup correctly, so I'm uncertain as to what's going on here...

@xster
Copy link
Member

xster commented Nov 5, 2019

@bkonyi, @collinjackson, anything blocking this PR currently?

@bkonyi
Copy link
Contributor Author

bkonyi commented Nov 5, 2019

@bkonyi, @collinjackson, anything blocking this PR currently?

Ah sorry for the delay. I think the migration is mostly done, but I'll need to upload my most recent changes. However, as discussed with @matthew-carroll and @mklim we're going to delay publishing this until the next stable is released to handle registering plugins with the background isolate automatically.

@bkonyi bkonyi force-pushed the android_alarm_manage_v2_embedding branch from 908cd1d to 010b7ea Compare November 5, 2019 20:48
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

public class BackgroundExecutionContext implements MethodCallHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

You may want to avoid the term Context on Android when not referring to an Android Context. It might be confusing for Android devs. Another name that might be more descriptive for this object could be FlutterBackgroundExecutor

Also, can you add an appropriate class javadoc?

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?

}

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

private Object initializationLock = new Object();
private MethodChannel alarmManagerPluginChannel;

private static AndroidAlarmManagerPlugin singleton;
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI, in the embedding I always place static members above instance members, and other places where I've created singletons I do so by calling it instance. Up to you if you'd like to replicate that here.


private AtomicBoolean isIsolateRunning = new AtomicBoolean(false);

private static PluginRegistrantCallback pluginRegistrantCallback;
Copy link
Contributor

Choose a reason for hiding this comment

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

If you'd like to match the embedding, all static members are placed above instance members.


private static PluginRegistrantCallback pluginRegistrantCallback;

public static void setPluginRegistrant(PluginRegistrantCallback callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a javadoc for this method? It's probably a good idea to point out when this is expected to be called, and also what kind of consequences might occur by failing to set this at the appropriate time.

return;
}

FlutterMain.ensureInitializationComplete(context, null);
Copy link
Contributor

Choose a reason for hiding this comment

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

On master this call should no longer be needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this still required for apps using the V1 embedding?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I think so. In short, if a Flutter experience is based on a FlutterEngine then that call is not needed. But if it's based on FlutterNativeView then it is.

Copy link
Contributor

@matthew-carroll matthew-carroll left a comment

Choose a reason for hiding this comment

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

LGTM with possibly one remaining question that I left.

This plugin is relatively complicated compared to others, and delves into areas of Android that are very difficult to reason about, so I'll just add that beyond an LGTM, I encourage us to exercise this plugin wherever, whenever, and however we can. If any bugs exist, we're likely to only realize it with a stacktrace.

@bkonyi
Copy link
Contributor Author

bkonyi commented Nov 14, 2019

LGTM with possibly one remaining question that I left.

This plugin is relatively complicated compared to others, and delves into areas of Android that are very difficult to reason about, so I'll just add that beyond an LGTM, I encourage us to exercise this plugin wherever, whenever, and however we can. If any bugs exist, we're likely to only realize it with a stacktrace.

FYI, I've got changes addressing your earlier comments that are a WIP but I won't get to them until I'm physically in the office next week (I'm not setup for Android dev on my MacBook at the moment). I'll do my best to fix up documentation to make this plugin's behavior as clear as possible.

I'm wondering what would be the best way to exercise this plugin other than the rather simplistic tests this PR adds. If we had more official plugins supporting background execution, I'd imagine that looking into having a dedicated device running applications triggering background execution events would be something worth doing, but other than that I'm not sure what we can do.

@bkonyi
Copy link
Contributor Author

bkonyi commented Nov 18, 2019

@collinjackson it looks like the tests are failing, but I'm not sure why... is the test being run with flutter test on Cirrus?

@xster
Copy link
Member

xster commented Nov 21, 2019

Seems like the error on firebase test lab is

Permission Denial: starting instrumentation ComponentInfo{io.flutter.plugins.androidalarmmanagerexample.test/androidx.test.runner.AndroidJUnitRunner} from pid=18303, uid=18303 not allowed because package io.flutter.plugins.androidalarmmanagerexample.test does not have a signature matching the target io.flutter.plugins.androidalarmmanagerexample

@bkonyi
Copy link
Contributor Author

bkonyi commented Nov 22, 2019

So it looks like the timeout on Firebase is too aggressive for the e2e test to pass on API 28+ and we timeout. If you look at the logcat output, we do see an incrementCounter: 1 in the logs after the test has been cancelled.

@collinjackson @amirh, any suggestions?

@collinjackson
Copy link
Contributor

So it looks like the timeout on Firebase is too aggressive for the e2e test to pass on API 28+ and we timeout. If you look at the logcat output, we do see an incrementCounter: 1 in the logs after the test has been cancelled.

@collinjackson @amirh, any suggestions?

I'm in favor of longer (5m?) timeouts to reduce flakiness if that actually causes the test to pass. We could also make the timeouts configurable per plugin but maybe that's overthinking it.

@bkonyi
Copy link
Contributor Author

bkonyi commented Nov 26, 2019

Finally got all the checks passing... Any objections to me landing this?

@vkammerer
Copy link

Really looking forward to the publication of this update!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants