diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 85a74cd7c93f1..d96769394603f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -543,6 +543,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index d6dfd85120359..80fda97ad63db 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -128,6 +128,7 @@ action("flutter_shell_java") { "io/flutter/embedding/android/FlutterTextureView.java", "io/flutter/embedding/android/FlutterView.java", "io/flutter/embedding/engine/FlutterEngine.java", + "io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java", "io/flutter/embedding/engine/FlutterEnginePluginRegistry.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterShellArgs.java", @@ -216,6 +217,8 @@ action("flutter_shell_java") { "//third_party/android_support/android_support_annotations.jar", "//third_party/android_support/android_support_fragment.jar", "//third_party/android_support/android_arch_lifecycle_common.jar", + "//third_party/android_support/android_arch_lifecycle_common_java8.jar", + "//third_party/android_support/android_arch_lifecycle_runtime.jar", "//third_party/android_support/android_arch_lifecycle_viewmodel.jar", ] diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 34a6aad3730fd..d77a06f1fcc88 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -274,9 +274,26 @@ protected FlutterFragment createFlutterFragment() { .flutterShellArgs(FlutterShellArgs.fromIntent(getIntent())) .renderMode(FlutterView.RenderMode.surface) .transparencyMode(FlutterView.TransparencyMode.opaque) + .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) .build(); } + /** + * Hook for subclasses to control whether or not the {@link FlutterFragment} within this + * {@code Activity} automatically attaches its {@link FlutterEngine} to this {@code Activity}. + *
+ * For an explanation of why this control exists, see {@link FlutterFragment.Builder#shouldAttachEngineToActivity()}. + *
+ * This property is controlled with a protected method instead of an {@code Intent} argument because + * the only situation where changing this value would help, is a situation in which + * {@code FlutterActivity} is being subclassed to utilize a custom and/or cached {@link FlutterEngine}. + *
+ * Defaults to {@code true}. + */ + protected boolean shouldAttachEngineToActivity() { + return true; + } + @Override public void onPostResume() { super.onPostResume(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 9dc152917dd94..b5596ac3e6184 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -66,6 +66,7 @@ public class FlutterFragment extends Fragment { protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args"; protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode"; protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode"; + protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity"; /** * Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond @@ -118,6 +119,7 @@ public static class Builder { private FlutterShellArgs shellArgs = null; private FlutterView.RenderMode renderMode = FlutterView.RenderMode.surface; private FlutterView.TransparencyMode transparencyMode = FlutterView.TransparencyMode.transparent; + private boolean shouldAttachEngineToActivity = true; /** * Constructs a {@code Builder} that is configured to construct an instance of @@ -199,6 +201,46 @@ public Builder transparencyMode(@NonNull FlutterView.TransparencyMode transparen return this; } + /** + * Whether or not this {@code FlutterFragment} should automatically attach its + * {@code Activity} as a control surface for its {@link FlutterEngine}. + *
+ * Control surfaces are used to provide Android resources and lifecycle events to + * plugins that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity} + * is true then this {@code FlutterFragment} will connect its {@link FlutterEngine} to the + * surrounding {@code Activity}, along with any plugins that are registered with that + * {@link FlutterEngine}. This allows plugins to access the {@code Activity}, as well as + * receive {@code Activity}-specific calls, e.g., {@link android.app.Activity#onNewIntent(Intent)}. + * If {@code shouldAttachEngineToActivity} is false, then this {@code FlutterFragment} will not + * automatically manage the connection between its {@link FlutterEngine} and the surrounding + * {@code Activity}. The {@code Activity} will need to be manually connected to this + * {@code FlutterFragment}'s {@link FlutterEngine} by the app developer. See + * {@link FlutterEngine#getActivityControlSurface()}. + *
+ * One reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to move the + * {@link FlutterEngine} somewhere else. For example, a developer might want the + * {@link FlutterEngine} to outlive the surrounding {@code Activity} so that it can be used + * later in a different {@code Activity}. To accomplish this, the {@link FlutterEngine} will + * need to be disconnected from the surrounding {@code Activity} at an unusual time, preventing + * this {@code FlutterFragment} from correctly managing the relationship between the + * {@link FlutterEngine} and the surrounding {@code Activity}. + *
+ * Another reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or explicitly + * control when the {@link FlutterEngine}'s plugins have access to the surrounding {@code Activity}. + * For example, imagine that this {@code FlutterFragment} only takes up part of the screen and + * the app developer wants to ensure that none of the Flutter plugins are able to manipulate + * the surrounding {@code Activity}. In this case, the developer would not want the + * {@link FlutterEngine} to have access to the {@code Activity}, which can be accomplished by + * setting {@code shouldAttachEngineToActivity} to {@code false}. + */ + @NonNull + public Builder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + /** * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. *
@@ -217,6 +259,7 @@ protected Bundle createArgs() { } args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name()); args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); return args; } @@ -303,10 +346,12 @@ public void onAttach(Context context) { // use-cases. platformPlugin = new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); - // Notify any plugins that are currently attached to our FlutterEngine that they - // are now attached to an Activity. - // TODO(mattcarroll): send in a real lifecycle. - flutterEngine.getActivityControlSurface().attachToActivity(getActivity(), null); + if (shouldAttachEngineToActivity()) { + // Notify any plugins that are currently attached to our FlutterEngine that they + // are now attached to an Activity. + // TODO(mattcarroll): send in a real lifecycle. + flutterEngine.getActivityControlSurface().attachToActivity(getActivity(), null); + } } private void initializeFlutter(@NonNull Context context) { @@ -543,9 +588,11 @@ public void onDetach() { super.onDetach(); Log.d(TAG, "onDetach()"); - // Notify plugins that they are no longer attached to an Activity. - // TODO(mattcarroll): differentiate between detaching for config changes and otherwise. - flutterEngine.getActivityControlSurface().detachFromActivity(); + if (shouldAttachEngineToActivity()) { + // Notify plugins that they are no longer attached to an Activity. + // TODO(mattcarroll): differentiate between detaching for config changes and otherwise. + flutterEngine.getActivityControlSurface().detachFromActivity(); + } // Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment, // and this Fragment's Activity. @@ -572,6 +619,10 @@ protected boolean retainFlutterEngineAfterFragmentDestruction() { return false; } + protected boolean shouldAttachEngineToActivity() { + return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY); + } + /** * The hardware back button was pressed. * diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 1a7612dec8593..452804df581b9 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -4,6 +4,8 @@ package io.flutter.embedding.engine; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; import android.content.Context; import android.support.annotation.NonNull; @@ -48,7 +50,7 @@ * a {@link io.flutter.embedding.android.FlutterView} as a {@link FlutterRenderer.RenderSurface}. */ // TODO(mattcarroll): re-evaluate system channel APIs - some are not well named or differentiated -public class FlutterEngine { +public class FlutterEngine implements LifecycleOwner { private static final String TAG = "FlutterEngine"; @NonNull @@ -59,6 +61,8 @@ public class FlutterEngine { private final DartExecutor dartExecutor; @NonNull private final FlutterEnginePluginRegistry pluginRegistry; + @NonNull + private final FlutterEngineAndroidLifecycle androidLifecycle; // System channels. @NonNull @@ -125,11 +129,11 @@ public FlutterEngine(@NonNull Context context) { systemChannel = new SystemChannel(dartExecutor); textInputChannel = new TextInputChannel(dartExecutor); - // TODO(mattcarroll): bring in Lifecycle. + androidLifecycle = new FlutterEngineAndroidLifecycle(this); this.pluginRegistry = new FlutterEnginePluginRegistry( context.getApplicationContext(), this, - null + androidLifecycle ); } @@ -154,7 +158,8 @@ private boolean isAttachedToJni() { * This {@code FlutterEngine} instance should be discarded after invoking this method. */ public void destroy() { - pluginRegistry.removeAll(); + // The order that these things are destroyed is important. + pluginRegistry.destroy(); dartExecutor.onDetachedFromJNI(); flutterJNI.removeEngineLifecycleListener(engineLifecycleListener); flutterJNI.detachFromNativeAndReleaseResources(); @@ -288,6 +293,13 @@ public ContentProviderControlSurface getContentProviderControlSurface() { return pluginRegistry; } + // TODO(mattcarroll): determine if we really need to expose this from FlutterEngine vs making PluginBinding a LifecycleOwner + @NonNull + @Override + public Lifecycle getLifecycle() { + return androidLifecycle; + } + /** * Lifecycle callbacks for Flutter engine lifecycle events. */ diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java new file mode 100644 index 0000000000000..671a6ecbbbb7e --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java @@ -0,0 +1,126 @@ +package io.flutter.embedding.engine; + +import android.arch.lifecycle.DefaultLifecycleObserver; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LifecycleRegistry; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Android {@link Lifecycle} that is owned by a {@link FlutterEngine}. + *
+ * {@code FlutterEngineAndroidLifecycle} exists so that {@code FlutterPlugin}s can monitor Android + * lifecycle events. When the associated {@link FlutterEngine} is running in an {@code Activity}, + * that {@code Activity}'s {@link Lifecycle} can be set as the {@code backingLifecycle} of this + * class, allowing all Flutter plugins to receive the {@code Activity}'s lifecycle events. Likewise, + * when the associated {@link FlutterEngine} is running in a {@code Service}, that {@code Service}'s + * {@link Lifecycle} can be set as the {@code backingLifecycle}. + *
+ * Sometimes a {@link FlutterEngine} exists in a non-lifecycle location, e.g., an {@code Application}, + * {@code ContentProvider}, or {@code BroadcastReceiver}. In these cases, this lifecycle reports + * itself in the {@link Lifecycle.State#CREATED} state. + *
+ * Regardless of what happens to a backing {@code Activity} or @{code Service}, this lifecycle
+ * will only report itself as {@link Lifecycle.State#DESTROYED} when the associated {@link FlutterEngine}
+ * itself is destroyed. This is because a {@link Lifecycle} is not allowed to emit any events after
+ * going to the {@link Lifecycle.State#DESTROYED} state. Thus, this lifecycle cannot emit such an
+ * event until its associated {@link FlutterEngine} is destroyed. This then begs the question, what
+ * happens when the backing {@code Activity} or {@code Service} is destroyed? This lifecycle will
+ * report the process up to the {@link Lifecycle.Event#ON_STOP} event, but will ignore the
+ * {@link Lifecycle.Event#ON_DESTROY} event. At that point, this lifecycle will be back in its
+ * default {@link Lifecycle.State#CREATED} state until some other backing {@link Lifecycle} is
+ * registered.
+ */
+final class FlutterEngineAndroidLifecycle extends LifecycleRegistry {
+ private static final String TAG = "FlutterEngineAndroidLifecycle";
+
+ @Nullable
+ private Lifecycle backingLifecycle;
+ private boolean isDestroyed = false;
+
+ private final LifecycleObserver forwardingObserver = new DefaultLifecycleObserver() {
+ @Override
+ public void onCreate(@NonNull LifecycleOwner owner) {
+ // No-op. The FlutterEngine's Lifecycle is always at least Created
+ // until it is Destroyed, so we ignore onCreate() events from
+ // backing Lifecycles.
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ handleLifecycleEvent(Event.ON_START);
+ }
+
+ @Override
+ public void onResume(@NonNull LifecycleOwner owner) {
+ handleLifecycleEvent(Event.ON_RESUME);
+ }
+
+ @Override
+ public void onPause(@NonNull LifecycleOwner owner) {
+ handleLifecycleEvent(Event.ON_PAUSE);
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ handleLifecycleEvent(Event.ON_STOP);
+ }
+
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner owner) {
+ // No-op. We don't allow FlutterEngine's Lifecycle to report destruction
+ // until the FlutterEngine itself is destroyed. This is because a Lifecycle
+ // is contractually obligated to send no more event once it gets to the
+ // Destroyed state, which would prevent FlutterEngine from switching to
+ // the next Lifecycle that is attached.
+ }
+ };
+
+ FlutterEngineAndroidLifecycle(@NonNull LifecycleOwner provider) {
+ super(provider);
+ }
+
+ public void setBackingLifecycle(@Nullable Lifecycle lifecycle) {
+ ensureNotDestroyed();
+
+ // We no longer want to propagate events from the old Lifecycle. Deregister our forwarding observer.
+ if (backingLifecycle != null) {
+ backingLifecycle.removeObserver(forwardingObserver);
+ }
+
+ // Manually move us to the Stopped state before we switch out the underlying Lifecycle.
+ handleLifecycleEvent(Event.ON_STOP);
+
+ // Switch out the underlying lifecycle.
+ backingLifecycle = lifecycle;
+
+ if (backingLifecycle != null) {
+ // Add our forwardingObserver to the new backing Lifecycle so that this PluginRegistry is
+ // controlled by that backing lifecycle. Adding our forwarding observer will automatically
+ // result in invocations of the necessary Lifecycle events to bring us up to speed with the
+ // new backingLifecycle, e.g., onStart(), onResume().
+ lifecycle.addObserver(forwardingObserver);
+ }
+ }
+
+ @Override
+ public void handleLifecycleEvent(@NonNull Event event) {
+ ensureNotDestroyed();
+ super.handleLifecycleEvent(event);
+ }
+
+ public void destroy() {
+ ensureNotDestroyed();
+ setBackingLifecycle(null);
+ markState(State.DESTROYED);
+ isDestroyed = true;
+ }
+
+ private void ensureNotDestroyed() {
+ if (isDestroyed) {
+ throw new IllegalStateException("Tried to invoke a method on a destroyed FlutterEngineAndroidLifecycle.");
+ }
+ }
+}
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
index d79b9d52b41fb..3d99e397bb70d 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
@@ -47,6 +47,7 @@ class FlutterEnginePluginRegistry implements PluginRegistry,
// Standard FlutterPlugin
private final FlutterPlugin.FlutterPluginBinding pluginBinding;
+ private final FlutterEngineAndroidLifecycle flutterEngineAndroidLifecycle;
// ActivityAware
private final Map
* A {@link FlutterEngine} may move from foreground to background, from an {@code Activity} to
- * a {@code Service}, and {@code FlutterPluginBinding}'s {@code lifecycle} generalizes those
+ * a {@code Service}. {@code FlutterPluginBinding}'s {@code lifecycle} generalizes those
* lifecycles so that a {@code FlutterPlugin} can react to lifecycle events without being
* concerned about which Android Component is currently holding the {@link FlutterEngine}.
* TODO(mattcarroll): add info about ActivityAware and ServiceAware for plugins that care.
*/
- class FlutterPluginBinding {
+ class FlutterPluginBinding implements LifecycleOwner {
private final Context applicationContext;
private final FlutterEngine flutterEngine;
private final Lifecycle lifecycle;
@@ -117,6 +118,7 @@ public FlutterEngine getFlutterEngine() {
return flutterEngine;
}
+ @Override
@NonNull
public Lifecycle getLifecycle() {
return lifecycle;
diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityControlSurface.java b/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityControlSurface.java
index 52e1ab3f44f14..a99d41b5783ed 100644
--- a/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityControlSurface.java
+++ b/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityControlSurface.java
@@ -66,7 +66,7 @@ public interface ActivityControlSurface {
* This method gives each {@link ActivityAware} plugin an opportunity to re-establish necessary
* references to the given {@link Activity}.
*/
- void reattachToActivityAfterConfigChange(@NonNull Activity activity);
+ void reattachToActivityAfterConfigChange(@NonNull Activity activity, @NonNull Lifecycle lifecycle);
/**
* Call this method from the {@link Activity} that is attached to this {@code ActivityControlSurfaces}'s
diff --git a/tools/android_support/files.json b/tools/android_support/files.json
index 6c673e7fff8e6..565e143b731b3 100644
--- a/tools/android_support/files.json
+++ b/tools/android_support/files.json
@@ -1,5 +1,7 @@
{
"android_arch_lifecycle_common.jar": "https://dl.google.com/dl/android/maven2/android/arch/lifecycle/common/1.1.1/common-1.1.1.jar",
+ "android_arch_lifecycle_common_java8.jar": "https://dl.google.com/dl/android/maven2/android/arch/lifecycle/common-java8/1.1.1/common-java8-1.1.1.jar",
+ "android_arch_lifecycle_runtime.jar": "https://dl.google.com/dl/android/maven2/android/arch/lifecycle/runtime/1.1.1/runtime-1.1.1.aar",
"android_arch_lifecycle_viewmodel.jar": "https://dl.google.com/dl/android/maven2/android/arch/lifecycle/viewmodel/1.1.1/viewmodel-1.1.1.aar",
"android_support_fragment.jar": "https://dl.google.com/dl/android/maven2/com/android/support/support-fragment/28.0.0/support-fragment-28.0.0.aar",
"android_support_v13.jar": "https://dl.google.com/dl/android/maven2/com/android/support/support-v13/28.0.0/support-v13-28.0.0.aar",