Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* For an explanation of why this control exists, see {@link FlutterFragment.Builder#shouldAttachEngineToActivity()}.
* <p>
* 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}.
* <p>
* Defaults to {@code true}.
*/
protected boolean shouldAttachEngineToActivity() {
return true;
}

@Override
public void onPostResume() {
super.onPostResume();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}.
* <p>
* 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()}.
* <p>
* 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}.
* <p>
* 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}.
* <p>
Expand All @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -59,6 +61,8 @@ public class FlutterEngine {
private final DartExecutor dartExecutor;
@NonNull
private final FlutterEnginePluginRegistry pluginRegistry;
@NonNull
private final FlutterEngineAndroidLifecycle androidLifecycle;

// System channels.
@NonNull
Expand Down Expand Up @@ -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
);
}

Expand All @@ -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();
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* {@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}.
* <p>
* 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.
* <p>
* Regardless of what happens to a backing {@code Activity} or @{code Service}, this lifecycle
Copy link
Contributor

Choose a reason for hiding this comment

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

It's unexpected to me that onDestroyed would be special cased to the FlutterEngine specifically and that it's impossible to get an onDestroyed callback for the actual underlying lifecycle. Any way we could propagate this information life normal without breaking any kind of Engine contract? Add in an onEngineDestroyed method just for this class to handle the final engine destruction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The contract that we're trying not to violate is the Lifecycle contract, not the FlutterEngine contract: https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.State#destroyed

The unique thing about FlutterEngine is that its backing Lifecycle can change over time. It goes from Activity to Service to nothing to Activity again. There is nothing preventing us from forwarding an Activity's ON_DESTROY event, but then any other code that is designed to understand Lifecycle events will have undefined behavior when the FlutterEngine's Lifecycle turns right around and emits an ON_CREATED event. Additionally, it is also conceptually incorrect to send that ON_DESTROY event because even though the Activity has been destroyed, the FlutterEngine remains, and at the end of the day this Lifecycle is truly owned by the FlutterEngine, it just temporarily takes on the Lifecycle of various Android components.

Does that make sense?

* 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.");
}
}
}
Loading