diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 2211299487ad3..bfa1b74aed56d 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -708,6 +708,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegist
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/DrawableSplashScreen.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
@@ -727,7 +728,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Splas
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/TransparencyMode.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java
-FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.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 3443dba51d3ae..52373e3c74ffa 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -128,6 +128,7 @@ android_java_sources = [
"io/flutter/embedding/android/AndroidKeyProcessor.java",
"io/flutter/embedding/android/AndroidTouchProcessor.java",
"io/flutter/embedding/android/DrawableSplashScreen.java",
+ "io/flutter/embedding/android/ExclusiveAppComponent.java",
"io/flutter/embedding/android/FlutterActivity.java",
"io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java",
"io/flutter/embedding/android/FlutterActivityLaunchConfigs.java",
@@ -147,7 +148,7 @@ android_java_sources = [
"io/flutter/embedding/android/TransparencyMode.java",
"io/flutter/embedding/engine/FlutterEngine.java",
"io/flutter/embedding/engine/FlutterEngineCache.java",
- "io/flutter/embedding/engine/FlutterEnginePluginRegistry.java",
+ "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java",
"io/flutter/embedding/engine/FlutterJNI.java",
"io/flutter/embedding/engine/FlutterOverlaySurface.java",
"io/flutter/embedding/engine/FlutterShellArgs.java",
@@ -429,7 +430,7 @@ action("robolectric_tests") {
"test/io/flutter/embedding/android/FlutterViewTest.java",
"test/io/flutter/embedding/android/RobolectricFlutterActivity.java",
"test/io/flutter/embedding/engine/FlutterEngineCacheTest.java",
- "test/io/flutter/embedding/engine/FlutterEnginePluginRegistryTest.java",
+ "test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java",
"test/io/flutter/embedding/engine/FlutterEngineTest.java",
"test/io/flutter/embedding/engine/FlutterJNITest.java",
"test/io/flutter/embedding/engine/FlutterShellArgsTest.java",
diff --git a/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java b/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java
new file mode 100644
index 0000000000000..e1c356a6ea6b4
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java
@@ -0,0 +1,35 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.embedding.android;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An Android App Component exclusively attached to a {@link
+ * io.flutter.embedding.engine.FlutterEngine}.
+ *
+ *
An exclusive App Component's {@link #detachFromFlutterEngine} is invoked when another App
+ * Component is becoming attached to the {@link io.flutter.embedding.engine.FlutterEngine}.
+ *
+ *
The term "App Component" refer to the 4 component types: Activity, Service, Broadcast
+ * Receiver, and Content Provider, as defined in
+ * https://developer.android.com/guide/components/fundamentals.
+ *
+ * @param The App Component behind this exclusive App Component.
+ */
+public interface ExclusiveAppComponent {
+ /**
+ * Called when another App Component is about to become attached to the {@link
+ * io.flutter.embedding.engine.FlutterEngine} this App Component is currently attached to.
+ *
+ * This App Component's connections to the {@link io.flutter.embedding.engine.FlutterEngine}
+ * are still valid at the moment of this call.
+ */
+ void detachFromFlutterEngine();
+
+ /** Retrieve the App Component behind this exclusive App Component. */
+ @NonNull
+ T getAppComponent();
+}
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
index 22993f08fe58e..66c2acd8e886b 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
@@ -560,56 +560,102 @@ protected void onPause() {
@Override
protected void onStop() {
super.onStop();
- delegate.onStop();
+ if (stillAttachedForEvent("onStop")) {
+ delegate.onStop();
+ }
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- delegate.onSaveInstanceState(outState);
+ if (stillAttachedForEvent("onSaveInstanceState")) {
+ delegate.onSaveInstanceState(outState);
+ }
+ }
+
+ /**
+ * Irreversibly release this activity's control of the {@link FlutterEngine} and its
+ * subcomponents.
+ *
+ *
Calling will disconnect this activity's view from the Flutter renderer, disconnect this
+ * activity from plugins' {@link ActivityControlSurface}, and stop system channel messages from
+ * this activity.
+ *
+ *
After calling, this activity should be disposed immediately and not be re-used.
+ */
+ private void release() {
+ delegate.onDestroyView();
+ delegate.onDetach();
+ delegate.release();
+ delegate = null;
+ }
+
+ @Override
+ public void detachFromFlutterEngine() {
+ Log.v(
+ TAG,
+ "FlutterActivity "
+ + this
+ + " connection to the engine "
+ + getFlutterEngine()
+ + " evicted by another attaching activity");
+ release();
}
@Override
protected void onDestroy() {
super.onDestroy();
- delegate.onDestroyView();
- delegate.onDetach();
+ if (stillAttachedForEvent("onDestroy")) {
+ release();
+ }
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- delegate.onActivityResult(requestCode, resultCode, data);
+ if (stillAttachedForEvent("onActivityResult")) {
+ delegate.onActivityResult(requestCode, resultCode, data);
+ }
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
// TODO(mattcarroll): change G3 lint rule that forces us to call super
super.onNewIntent(intent);
- delegate.onNewIntent(intent);
+ if (stillAttachedForEvent("onNewIntent")) {
+ delegate.onNewIntent(intent);
+ }
}
@Override
public void onBackPressed() {
- delegate.onBackPressed();
+ if (stillAttachedForEvent("onBackPressed")) {
+ delegate.onBackPressed();
+ }
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (stillAttachedForEvent("onRequestPermissionsResult")) {
+ delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
}
@Override
public void onUserLeaveHint() {
- delegate.onUserLeaveHint();
+ if (stillAttachedForEvent("onUserLeaveHint")) {
+ delegate.onUserLeaveHint();
+ }
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
- delegate.onTrimMemory(level);
+ if (stillAttachedForEvent("onTrimMemory")) {
+ delegate.onTrimMemory(level);
+ }
}
/**
@@ -908,7 +954,7 @@ public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {
*
Returning false from this method does not preclude a {@link FlutterEngine} from being
* attaching to a {@code FlutterActivity} - it just prevents the attachment from happening
* automatically. A developer can choose to subclass {@code FlutterActivity} and then invoke
- * {@link ActivityControlSurface#attachToActivity(Activity, Lifecycle)} and {@link
+ * {@link ActivityControlSurface#attachToActivity(ExclusiveAppComponent, Lifecycle)} and {@link
* ActivityControlSurface#detachFromActivity()} at the desired times.
*
*
One reason that a developer might choose to manually manage the relationship between the
@@ -961,4 +1007,12 @@ public boolean shouldRestoreAndSaveState() {
}
return true;
}
+
+ private boolean stillAttachedForEvent(String event) {
+ if (delegate == null) {
+ Log.v(TAG, "FlutterActivity " + hashCode() + " " + event + " called after release.");
+ return false;
+ }
+ return true;
+ }
}
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
index e4cad301c21a0..78585172dd683 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@@ -62,7 +62,7 @@
* the same form. Do not use this class as a convenient shortcut for any other
* behavior.
*/
-/* package */ final class FlutterActivityAndFragmentDelegate {
+/* package */ class FlutterActivityAndFragmentDelegate implements ExclusiveAppComponent {
private static final String TAG = "FlutterActivityAndFragmentDelegate";
private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework";
private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins";
@@ -154,14 +154,6 @@ void onAttach(@NonNull Context context) {
setupFlutterEngine();
}
- // Regardless of whether or not a FlutterEngine already existed, the PlatformPlugin
- // is bound to a specific Activity. Therefore, it needs to be created and configured
- // every time this Fragment attaches to a new Activity.
- // TODO(mattcarroll): the PlatformPlugin needs to be reimagined because it implicitly takes
- // control of the entire window. This is unacceptable for non-fullscreen
- // use-cases.
- platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
-
if (host.shouldAttachEngineToActivity()) {
// Notify any plugins that are currently attached to our FlutterEngine that they
// are now attached to an Activity.
@@ -172,15 +164,32 @@ void onAttach(@NonNull Context context) {
// which means there shouldn't be any possibility for the Fragment Lifecycle to get out of
// sync with the Activity. We use the Fragment's Lifecycle because it is possible that the
// attached Activity is not a LifecycleOwner.
- Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this Fragment.");
- flutterEngine
- .getActivityControlSurface()
- .attachToActivity(host.getActivity(), host.getLifecycle());
+ Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
+ flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
+ // Regardless of whether or not a FlutterEngine already existed, the PlatformPlugin
+ // is bound to a specific Activity. Therefore, it needs to be created and configured
+ // every time this Fragment attaches to a new Activity.
+ // TODO(mattcarroll): the PlatformPlugin needs to be reimagined because it implicitly takes
+ // control of the entire window. This is unacceptable for non-fullscreen
+ // use-cases.
+ platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
+
host.configureFlutterEngine(flutterEngine);
}
+ @Override
+ public @NonNull Activity getAppComponent() {
+ final Activity activity = host.getActivity();
+ if (activity == null) {
+ throw new AssertionError(
+ "FlutterActivityAndFragmentDelegate's getAppComponent should only "
+ + "be queried after onAttach, when the host's activity should always be non-null");
+ }
+ return activity;
+ }
+
/**
* Obtains a reference to a FlutterEngine to back this delegate and its {@code host}.
*
@@ -480,6 +489,24 @@ void onSaveInstanceState(@Nullable Bundle bundle) {
}
}
+ @Override
+ public void detachFromFlutterEngine() {
+ if (host.shouldDestroyEngineWithHost()) {
+ // The host owns the engine and should never have its engine taken by another exclusive
+ // activity.
+ throw new AssertionError(
+ "The internal FlutterEngine created by "
+ + host
+ + " has been attached to by another activity. To persist a FlutterEngine beyond the "
+ + "ownership of this activity, explicitly create a FlutterEngine");
+ }
+
+ // Default, but customizable, behavior is for the host to call {@link #onDetach}
+ // deterministically as to not mix more events during the lifecycle of the next exclusive
+ // activity.
+ host.detachFromFlutterEngine();
+ }
+
/**
* Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDetach()}.
*
@@ -741,6 +768,15 @@ private void ensureAlive() {
*/
boolean shouldDestroyEngineWithHost();
+ /**
+ * Callback called when the {@link FlutterEngine} has been attached to by another activity
+ * before this activity was destroyed.
+ *
+ * The expected behavior is for this activity to synchronously stop using the {@link
+ * FlutterEngine} to avoid lifecycle crosstalk with the new activity.
+ */
+ void detachFromFlutterEngine();
+
/** Returns the Dart entrypoint that should run when a new {@link FlutterEngine} is created. */
@NonNull
String getDartEntrypointFunctionName();
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java b/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java
index 227770d4b0bd6..94dc318173e79 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java
@@ -4,7 +4,6 @@
package io.flutter.embedding.android;
-import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.FlutterEngine;
@@ -21,8 +20,8 @@ public interface FlutterEngineConfigurator {
*
*
This method is called after the given {@link FlutterEngine} has been attached to the owning
* {@code FragmentActivity}. See {@link
- * io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(Activity,
- * Lifecycle)}.
+ * io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(
+ * ExclusiveAppComponent, Lifecycle)}.
*
*
It is possible that the owning {@code FragmentActivity} opted not to connect itself as an
* {@link io.flutter.embedding.engine.plugins.activity.ActivityControlSurface}. In that case, any
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
index d64b6788e71a6..4d2438bd95935 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
@@ -623,29 +623,55 @@ public void onPause() {
@Override
public void onStop() {
super.onStop();
- delegate.onStop();
+ if (stillAttachedForEvent("onStop")) {
+ delegate.onStop();
+ }
}
@Override
public void onDestroyView() {
super.onDestroyView();
- delegate.onDestroyView();
+ if (stillAttachedForEvent("onDestroyView")) {
+ delegate.onDestroyView();
+ }
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- delegate.onSaveInstanceState(outState);
+ if (stillAttachedForEvent("onSaveInstanceState")) {
+ delegate.onSaveInstanceState(outState);
+ }
}
@Override
- public void onDetach() {
- super.onDetach();
+ public void detachFromFlutterEngine() {
+ Log.v(
+ TAG,
+ "FlutterFragment "
+ + this
+ + " connection to the engine "
+ + getFlutterEngine()
+ + " evicted by another attaching activity");
+ // Redundant calls are ok.
+ delegate.onDestroyView();
delegate.onDetach();
delegate.release();
delegate = null;
}
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (delegate != null) {
+ delegate.onDetach();
+ delegate.release();
+ delegate = null;
+ } else {
+ Log.v(TAG, "FlutterFragment " + this + " onDetach called after release.");
+ }
+ }
+
/**
* The result of a permission request has been received.
*
@@ -660,7 +686,9 @@ public void onDetach() {
@ActivityCallThrough
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (stillAttachedForEvent("onRequestPermissionsResult")) {
+ delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
}
/**
@@ -675,7 +703,9 @@ public void onRequestPermissionsResult(
*/
@ActivityCallThrough
public void onNewIntent(@NonNull Intent intent) {
- delegate.onNewIntent(intent);
+ if (stillAttachedForEvent("onNewIntent")) {
+ delegate.onNewIntent(intent);
+ }
}
/**
@@ -685,7 +715,9 @@ public void onNewIntent(@NonNull Intent intent) {
*/
@ActivityCallThrough
public void onBackPressed() {
- delegate.onBackPressed();
+ if (stillAttachedForEvent("onBackPressed")) {
+ delegate.onBackPressed();
+ }
}
/**
@@ -700,7 +732,9 @@ public void onBackPressed() {
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- delegate.onActivityResult(requestCode, resultCode, data);
+ if (stillAttachedForEvent("onActivityResult")) {
+ delegate.onActivityResult(requestCode, resultCode, data);
+ }
}
/**
@@ -711,7 +745,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
*/
@ActivityCallThrough
public void onUserLeaveHint() {
- delegate.onUserLeaveHint();
+ if (stillAttachedForEvent("onUserLeaveHint")) {
+ delegate.onUserLeaveHint();
+ }
}
/**
@@ -725,7 +761,9 @@ public void onUserLeaveHint() {
*/
@ActivityCallThrough
public void onTrimMemory(int level) {
- delegate.onTrimMemory(level);
+ if (stillAttachedForEvent("onTrimMemory")) {
+ delegate.onTrimMemory(level);
+ }
}
/**
@@ -736,7 +774,9 @@ public void onTrimMemory(int level) {
@Override
public void onLowMemory() {
super.onLowMemory();
- delegate.onLowMemory();
+ if (stillAttachedForEvent("onLowMemory")) {
+ delegate.onLowMemory();
+ }
}
/**
@@ -929,8 +969,8 @@ public PlatformPlugin providePlatformPlugin(
*
*
This method is called after {@link #provideFlutterEngine(Context)}, and after the given
* {@link FlutterEngine} has been attached to the owning {@code FragmentActivity}. See {@link
- * io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(Activity,
- * Lifecycle)}.
+ * io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(
+ * ExclusiveAppComponent, Lifecycle)}.
*
*
It is possible that the owning {@code FragmentActivity} opted not to connect itself as an
* {@link io.flutter.embedding.engine.plugins.activity.ActivityControlSurface}. In that case, any
@@ -1035,6 +1075,14 @@ public boolean shouldRestoreAndSaveState() {
return true;
}
+ private boolean stillAttachedForEvent(String event) {
+ if (delegate == null) {
+ Log.v(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release.");
+ return false;
+ }
+ return true;
+ }
+
/**
* Annotates methods in {@code FlutterFragment} that must be called by the containing {@code
* Activity}.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index 2f1942034a7e5..dde3dbaecdd00 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -967,7 +967,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
public void detachFromFlutterEngine() {
Log.v(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
if (!isAttachedToFlutterEngine()) {
- Log.v(TAG, "Not attached to an engine. Doing nothing.");
+ Log.v(TAG, "FlutterView not attached to an engine. Not detaching.");
return;
}
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
index 9ec50d7ef98eb..71730e4668a85 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
@@ -74,7 +74,7 @@ public class FlutterEngine {
@NonNull private final FlutterJNI flutterJNI;
@NonNull private final FlutterRenderer renderer;
@NonNull private final DartExecutor dartExecutor;
- @NonNull private final FlutterEnginePluginRegistry pluginRegistry;
+ @NonNull private final FlutterEngineConnectionRegistry pluginRegistry;
@NonNull private final LocalizationPlugin localizationPlugin;
// System channels.
@@ -301,7 +301,7 @@ public FlutterEngine(
this.platformViewsController.onAttachedToJNI();
this.pluginRegistry =
- new FlutterEnginePluginRegistry(context.getApplicationContext(), this, flutterLoader);
+ new FlutterEngineConnectionRegistry(context.getApplicationContext(), this, flutterLoader);
if (automaticallyRegisterPlugins) {
registerPlugins();
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java
similarity index 90%
rename from shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
rename to shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java
index 7e9d39b30752d..729bc1d842595 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java
@@ -15,6 +15,7 @@
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import io.flutter.Log;
+import io.flutter.embedding.android.ExclusiveAppComponent;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.PluginRegistry;
@@ -36,13 +37,20 @@
import java.util.Map;
import java.util.Set;
-class FlutterEnginePluginRegistry
+/**
+ * This class is owned by the {@link FlutterEngine} and its role is to managed its connections with
+ * Android App Components and Flutter plugins.
+ *
+ *
It enforces the {0|1}:1 relationship between activity and engine, and propagates the app
+ * component connection to the plugins.
+ */
+/* package */ class FlutterEngineConnectionRegistry
implements PluginRegistry,
ActivityControlSurface,
ServiceControlSurface,
BroadcastReceiverControlSurface,
ContentProviderControlSurface {
- private static final String TAG = "FlutterEnginePluginRegistry";
+ private static final String TAG = "FlutterEngineConnectionRegistry";
// PluginRegistry
@NonNull
@@ -57,7 +65,9 @@ class FlutterEnginePluginRegistry
private final Map, ActivityAware> activityAwarePlugins =
new HashMap<>();
- @Nullable private Activity activity;
+ // TODO(xster): remove activity after 2021/03/01 since exclusiveActivity should be the API to use.
+ @Deprecated @Nullable private Activity activity;
+ @Nullable private ExclusiveAppComponent exclusiveActivity;
@Nullable private FlutterEngineActivityPluginBinding activityPluginBinding;
private boolean isWaitingForActivityReattachment = false;
@@ -85,7 +95,7 @@ class FlutterEnginePluginRegistry
@Nullable private ContentProvider contentProvider;
@Nullable private FlutterEngineContentProviderPluginBinding contentProviderPluginBinding;
- FlutterEnginePluginRegistry(
+ FlutterEngineConnectionRegistry(
@NonNull Context appContext,
@NonNull FlutterEngine flutterEngine,
@NonNull FlutterLoader flutterLoader) {
@@ -103,10 +113,10 @@ class FlutterEnginePluginRegistry
public void destroy() {
Log.v(TAG, "Destroying.");
// Detach from any Android component that we may currently be attached to, e.g., Activity,
- // Service,
- // BroadcastReceiver, ContentProvider. This must happen before removing all plugins so that the
- // plugins have an opportunity to clean up references as a result of component detachment.
- detachFromAndroidComponent();
+ // Service, BroadcastReceiver, ContentProvider. This must happen before removing all plugins so
+ // that the plugins have an opportunity to clean up references as a result of component
+ // detachment.
+ detachFromAppComponent();
// Remove all registered plugins.
removeAll();
@@ -269,7 +279,7 @@ public void removeAll() {
plugins.clear();
}
- private void detachFromAndroidComponent() {
+ private void detachFromAppComponent() {
if (isAttachedToActivity()) {
detachFromActivity();
} else if (isAttachedToService()) {
@@ -283,7 +293,11 @@ private void detachFromAndroidComponent() {
// -------- Start ActivityControlSurface -------
private boolean isAttachedToActivity() {
- return activity != null;
+ return activity != null || exclusiveActivity != null;
+ }
+
+ private Activity attachedActivity() {
+ return exclusiveActivity != null ? exclusiveActivity.getAppComponent() : activity;
}
@Override
@@ -294,10 +308,43 @@ public void attachToActivity(@NonNull Activity activity, @NonNull Lifecycle life
+ activity
+ "."
+ (isWaitingForActivityReattachment ? " This is after a config change." : ""));
- // If we were already attached to an Android component, detach from it.
- detachFromAndroidComponent();
+ if (this.exclusiveActivity != null) {
+ this.exclusiveActivity.detachFromFlutterEngine();
+ }
+ // If we were already attached to an app component, detach from it.
+ detachFromAppComponent();
+ if (this.exclusiveActivity != null) {
+ throw new AssertionError("Only activity or exclusiveActivity should be set");
+ }
this.activity = activity;
+ attachToActivityInternal(activity, lifecycle);
+ }
+
+ @Override
+ public void attachToActivity(
+ @NonNull ExclusiveAppComponent exclusiveActivity, @NonNull Lifecycle lifecycle) {
+ Log.v(
+ TAG,
+ "Attaching to an exclusive Activity: "
+ + exclusiveActivity.getAppComponent()
+ + (isAttachedToActivity() ? " evicting previous activity " + attachedActivity() : "")
+ + "."
+ + (isWaitingForActivityReattachment ? " This is after a config change." : ""));
+ if (this.exclusiveActivity != null) {
+ this.exclusiveActivity.detachFromFlutterEngine();
+ }
+ // If we were already attached to an app component, detach from it.
+ detachFromAppComponent();
+
+ if (this.activity != null) {
+ throw new AssertionError("Only activity or exclusiveActivity should be set");
+ }
+ this.exclusiveActivity = exclusiveActivity;
+ attachToActivityInternal(exclusiveActivity.getAppComponent(), lifecycle);
+ }
+
+ private void attachToActivityInternal(@NonNull Activity activity, @NonNull Lifecycle lifecycle) {
this.activityPluginBinding = new FlutterEngineActivityPluginBinding(activity, lifecycle);
// Activate the PlatformViewsController. This must happen before any plugins attempt
@@ -321,18 +368,14 @@ public void attachToActivity(@NonNull Activity activity, @NonNull Lifecycle life
@Override
public void detachFromActivityForConfigChanges() {
if (isAttachedToActivity()) {
- Log.v(TAG, "Detaching from an Activity for config changes: " + activity);
+ Log.v(TAG, "Detaching from an Activity for config changes: " + attachedActivity());
isWaitingForActivityReattachment = true;
for (ActivityAware activityAware : activityAwarePlugins.values()) {
activityAware.onDetachedFromActivityForConfigChanges();
}
- // Deactivate PlatformViewsController.
- flutterEngine.getPlatformViewsController().detach();
-
- activity = null;
- activityPluginBinding = null;
+ detachFromActivityInternal();
} else {
Log.e(TAG, "Attempted to detach plugins from an Activity when no Activity was attached.");
}
@@ -341,21 +384,26 @@ public void detachFromActivityForConfigChanges() {
@Override
public void detachFromActivity() {
if (isAttachedToActivity()) {
- Log.v(TAG, "Detaching from an Activity: " + activity);
+ Log.v(TAG, "Detaching from an Activity: " + attachedActivity());
for (ActivityAware activityAware : activityAwarePlugins.values()) {
activityAware.onDetachedFromActivity();
}
- // Deactivate PlatformViewsController.
- flutterEngine.getPlatformViewsController().detach();
-
- activity = null;
- activityPluginBinding = null;
+ detachFromActivityInternal();
} else {
Log.e(TAG, "Attempted to detach plugins from an Activity when no Activity was attached.");
}
}
+ private void detachFromActivityInternal() {
+ // Deactivate PlatformViewsController.
+ flutterEngine.getPlatformViewsController().detach();
+
+ exclusiveActivity = null;
+ activity = null;
+ activityPluginBinding = null;
+ }
+
@Override
public boolean onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResult) {
@@ -443,7 +491,7 @@ public void attachToService(
@NonNull Service service, @Nullable Lifecycle lifecycle, boolean isForeground) {
Log.v(TAG, "Attaching to a Service: " + service);
// If we were already attached to an Android component, detach from it.
- detachFromAndroidComponent();
+ detachFromAppComponent();
this.service = service;
this.servicePluginBinding = new FlutterEngineServicePluginBinding(service, lifecycle);
@@ -497,7 +545,7 @@ public void attachToBroadcastReceiver(
@NonNull BroadcastReceiver broadcastReceiver, @NonNull Lifecycle lifecycle) {
Log.v(TAG, "Attaching to BroadcastReceiver: " + broadcastReceiver);
// If we were already attached to an Android component, detach from it.
- detachFromAndroidComponent();
+ detachFromAppComponent();
this.broadcastReceiver = broadcastReceiver;
this.broadcastReceiverPluginBinding =
@@ -539,7 +587,7 @@ public void attachToContentProvider(
@NonNull ContentProvider contentProvider, @NonNull Lifecycle lifecycle) {
Log.v(TAG, "Attaching to ContentProvider: " + contentProvider);
// If we were already attached to an Android component, detach from it.
- detachFromAndroidComponent();
+ detachFromAppComponent();
this.contentProvider = contentProvider;
this.contentProviderPluginBinding =
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 671f8311f4827..276b54d56321a 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
@@ -10,6 +10,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
+import io.flutter.embedding.android.ExclusiveAppComponent;
/**
* Control surface through which an {@link Activity} attaches to a {@link FlutterEngine}.
@@ -19,9 +20,10 @@
*
*
* - Once an {@link Activity} is created, and its associated {@link FlutterEngine} is executing
- * Dart code, the {@link Activity} should invoke {@link #attachToActivity(Activity,
- * Lifecycle)}. At this point the {@link FlutterEngine} is considered "attached" to the {@link
- * Activity} and all {@link ActivityAware} plugins are given access to the {@link Activity}.
+ * Dart code, the {@link Activity} should invoke {@link #attachToActivity(
+ * ExclusiveAppComponent, Lifecycle)}. At this point the {@link FlutterEngine} is considered
+ * "attached" to the {@link Activity} and all {@link ActivityAware} plugins are given access
+ * to the {@link Activity}.
*
- Just before an attached {@link Activity} is destroyed for configuration change purposes,
* that {@link Activity} should invoke {@link #detachFromActivityForConfigChanges()}, giving
* each {@link ActivityAware} plugin an opportunity to clean up its references before the
@@ -32,6 +34,10 @@
*
- When an {@link Activity} is destroyed for non-configuration-change purposes, or when the
* {@link Activity} is no longer interested in displaying a {@link FlutterEngine}'s content,
* the {@link Activity} should invoke {@link #detachFromActivity()}.
+ *
- When a {@link Activity} is being attached while an existing {@link ExclusiveAppComponent}
+ * is already attached, the existing {@link ExclusiveAppComponent} is given a chance to detach
+ * first via {@link ExclusiveAppComponent#detachFromFlutterEngine()} before the new activity
+ * attaches.
*
*
* The attached {@link Activity} should also forward all {@link Activity} calls that this {@code
@@ -48,9 +54,31 @@ public interface ActivityControlSurface {
* Dart code, the {@link Activity} should invoke this method. At that point the {@link
* FlutterEngine} is considered "attached" to the {@link Activity} and all {@link ActivityAware}
* plugins are given access to the {@link Activity}.
+ *
+ * @deprecated Prefer using the {@link #attachToActivity(ExclusiveAppComponent, Lifecycle)} API to
+ * avoid situations where multiple activities are driving the FlutterEngine simultaneously.
+ * See https://github.com/flutter/flutter/issues/66192.
*/
+ @Deprecated
void attachToActivity(@NonNull Activity activity, @NonNull Lifecycle lifecycle);
+ /**
+ * Call this method from the {@link ExclusiveAppComponent} that is displaying the visual content
+ * of the {@link FlutterEngine} that is associated with this {@code ActivityControlSurface}.
+ *
+ * Once an {@link ExclusiveAppComponent} is created, and its associated {@link FlutterEngine}
+ * is executing Dart code, the {@link ExclusiveAppComponent} should invoke this method. At that
+ * point the {@link FlutterEngine} is considered "attached" to the {@link ExclusiveAppComponent}
+ * and all {@link ActivityAware} plugins are given access to the {@link ExclusiveAppComponent}'s
+ * {@link Activity}.
+ *
+ *
This method differs from {@link #attachToActivity(Activity, Lifecycle)} in that it calls
+ * back the existing {@link ExclusiveAppComponent} to give it a chance to cleanly detach before a
+ * new {@link ExclusiveAppComponent} is attached.
+ */
+ void attachToActivity(
+ @NonNull ExclusiveAppComponent exclusiveActivity, @NonNull Lifecycle lifecycle);
+
/**
* Call this method from the {@link Activity} that is attached to this {@code
* ActivityControlSurfaces}'s {@link FlutterEngine} when the {@link Activity} is about to be
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index 75d724605c69d..c7de3c2c0ec6c 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -460,7 +460,9 @@ public void attach(
*/
@UiThread
public void detach() {
- platformViewsChannel.setPlatformViewsHandler(null);
+ if (platformViewsChannel != null) {
+ platformViewsChannel.setPlatformViewsHandler(null);
+ }
platformViewsChannel = null;
context = null;
textureRegistry = null;
diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
index bb5c44c6568df..bdc130ef87e9a 100644
--- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java
+++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
@@ -12,7 +12,7 @@
import io.flutter.embedding.android.FlutterFragmentTest;
import io.flutter.embedding.android.FlutterViewTest;
import io.flutter.embedding.engine.FlutterEngineCacheTest;
-import io.flutter.embedding.engine.FlutterEnginePluginRegistryTest;
+import io.flutter.embedding.engine.FlutterEngineConnectionRegistryTest;
import io.flutter.embedding.engine.FlutterJNITest;
import io.flutter.embedding.engine.LocalizationPluginTest;
import io.flutter.embedding.engine.RenderingComponentTest;
@@ -52,7 +52,7 @@
FlutterActivityTest.class,
FlutterAndroidComponentTest.class,
FlutterEngineCacheTest.class,
- FlutterEnginePluginRegistryTest.class,
+ FlutterEngineConnectionRegistryTest.class,
FlutterEngineTest.class,
FlutterFragmentActivityTest.class,
FlutterFragmentTest.class,
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index 7ef0b048e7e8f..4a621b3344107 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -357,7 +357,7 @@ public void itAttachesFlutterToTheActivityIfDesired() {
// Verify that the ActivityControlSurface was told to attach to an Activity.
verify(mockFlutterEngine.getActivityControlSurface(), times(1))
- .attachToActivity(any(Activity.class), any(Lifecycle.class));
+ .attachToActivity(any(ExclusiveAppComponent.class), any(Lifecycle.class));
// Flutter is detached from the surrounding Activity in onDetach.
delegate.onDetach();
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
index 67e5984fad0ec..2fe23de49b303 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
@@ -6,6 +6,9 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -15,6 +18,7 @@
import androidx.annotation.Nullable;
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode;
import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugins.GeneratedPluginRegistrant;
@@ -158,6 +162,40 @@ public void itRegistersPluginsAtConfigurationTime() {
assertEquals(activity.getFlutterEngine(), registeredEngines.get(0));
}
+ @Test
+ public void itCanBeDetachedFromTheEngineAndStopSendingFurtherEvents() {
+ FlutterActivityAndFragmentDelegate mockDelegate =
+ mock(FlutterActivityAndFragmentDelegate.class);
+ FlutterEngine mockEngine = mock(FlutterEngine.class);
+ FlutterEngineCache.getInstance().put("my_cached_engine", mockEngine);
+
+ Intent intent =
+ FlutterActivity.withCachedEngine("my_cached_engine").build(RuntimeEnvironment.application);
+ ActivityController activityController =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ FlutterActivity flutterActivity = activityController.get();
+ flutterActivity.setDelegate(mockDelegate);
+ flutterActivity.onStart();
+ flutterActivity.onResume();
+
+ verify(mockDelegate, times(1)).onStart();
+ verify(mockDelegate, times(1)).onResume();
+
+ flutterActivity.onPause();
+ flutterActivity.detachFromFlutterEngine();
+ verify(mockDelegate, times(1)).onPause();
+ verify(mockDelegate, times(1)).onDestroyView();
+ verify(mockDelegate, times(1)).onDetach();
+
+ flutterActivity.onStop();
+ flutterActivity.onDestroy();
+
+ verify(mockDelegate, never()).onStop();
+ // 1 time same as before.
+ verify(mockDelegate, times(1)).onDestroyView();
+ verify(mockDelegate, times(1)).onDetach();
+ }
+
static class FlutterActivityWithProvidedEngine extends FlutterActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
index 9255cb4077b67..5cede33a1b833 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java
@@ -3,15 +3,19 @@
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.app.Activity;
import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,15 +28,18 @@
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.plugin.platform.PlatformPlugin;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@@ -54,7 +61,8 @@ public void pluginsReceiveFlutterPluginBinding() {
cachedEngine.getPlugins().add(mockPlugin);
// Create a fake Host, which is required by the delegate.
- FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine);
+ FakeHost fakeHost = new FakeHost(cachedEngine);
+ fakeHost.shouldDestroyEngineWithHost = true;
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost);
@@ -165,9 +173,87 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
verify(activityAwarePlugin, times(1)).onDetachedFromActivity();
}
+ @Test
+ public void normalLifecycleStepsDoNotTriggerADetachFromFlutterEngine() {
+ // ---- Test setup ----
+ // Place a FlutterEngine in the static cache.
+ FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
+ FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
+ when(mockFlutterJni.isAttached()).thenReturn(true);
+ FlutterEngine cachedEngine =
+ spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
+ FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
+
+ // Create a fake Host, which is required by the delegate.
+ FakeHost fakeHost = new FakeHost(cachedEngine);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate =
+ spy(new FlutterActivityAndFragmentDelegate(fakeHost));
+
+ // --- Execute the behavior under test ---
+ // Push the delegate through all lifecycle methods all the way to destruction.
+ delegate.onAttach(RuntimeEnvironment.application);
+ delegate.onActivityCreated(null);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+ delegate.onPause();
+ delegate.onStop();
+ delegate.onDestroyView();
+ delegate.onDetach();
+
+ verify(delegate, never()).detachFromFlutterEngine();
+ }
+
+ @Test
+ public void twoOverlappingFlutterActivitiesDoNotCrosstalk() {
+ // ---- Test setup ----
+ // Place a FlutterEngine in the static cache.
+ FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
+ FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
+ when(mockFlutterJni.isAttached()).thenReturn(true);
+ FlutterEngine cachedEngine =
+ spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
+ FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
+ LifecycleChannel mockLifecycleChannel = mock(LifecycleChannel.class);
+ when(cachedEngine.getLifecycleChannel()).thenReturn(mockLifecycleChannel);
+
+ Intent intent =
+ FlutterActivity.withCachedEngine("my_flutter_engine").build(RuntimeEnvironment.application);
+ ActivityController activityController1 =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ activityController1.create().start().resume();
+
+ InOrder inOrder = inOrder(mockLifecycleChannel);
+ inOrder.verify(mockLifecycleChannel, times(1)).appIsResumed();
+ verifyNoMoreInteractions(mockLifecycleChannel);
+
+ activityController1.pause();
+ // Create a second instance on the same engine and start running it as well.
+ ActivityController activityController2 =
+ Robolectric.buildActivity(FlutterActivity.class, intent);
+ activityController2.create().start().resume();
+
+ // From the onPause of the first activity.
+ inOrder.verify(mockLifecycleChannel, times(1)).appIsInactive();
+ // By creating the second activity, we should automatically detach the first activity.
+ inOrder.verify(mockLifecycleChannel, times(1)).appIsDetached();
+ // In order, the second activity then is resumed.
+ inOrder.verify(mockLifecycleChannel, times(1)).appIsResumed();
+ verifyNoMoreInteractions(mockLifecycleChannel);
+
+ // The first activity goes through the normal lifecycles of destruction, but since we
+ // detached the first activity during the second activity's creation, we should ignore the
+ // first activity's destruction events to avoid crosstalk.
+ activityController1.stop().destroy();
+ verifyNoMoreInteractions(mockLifecycleChannel);
+ }
+
private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host {
final FlutterEngine cachedEngine;
Activity activity;
+ boolean shouldDestroyEngineWithHost = false;
Lifecycle lifecycle = mock(Lifecycle.class);
private FakeHost(@NonNull FlutterEngine flutterEngine) {
@@ -209,7 +295,7 @@ public String getCachedEngineId() {
@Override
public boolean shouldDestroyEngineWithHost() {
- return true;
+ return shouldDestroyEngineWithHost;
}
@NonNull
@@ -288,5 +374,8 @@ public void onFlutterUiDisplayed() {}
@Override
public void onFlutterUiNoLongerDisplayed() {}
+
+ @Override
+ public void detachFromFlutterEngine() {}
}
}
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
index 6dea8ec6b946d..9327994e8d9e6 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java
@@ -5,6 +5,10 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -71,4 +75,34 @@ public void itCreatesCachedEngineFragmentThatDestroysTheEngine() {
assertEquals("my_cached_engine", fragment.getCachedEngineId());
assertTrue(fragment.shouldDestroyEngineWithHost());
}
+
+ @Test
+ public void itCanBeDetachedFromTheEngineAndStopSendingFurtherEvents() {
+ FlutterActivityAndFragmentDelegate mockDelegate =
+ mock(FlutterActivityAndFragmentDelegate.class);
+ FlutterFragment fragment =
+ FlutterFragment.withCachedEngine("my_cached_engine")
+ .destroyEngineWithFragment(true)
+ .build();
+ fragment.setDelegate(mockDelegate);
+ fragment.onStart();
+ fragment.onResume();
+
+ verify(mockDelegate, times(1)).onStart();
+ verify(mockDelegate, times(1)).onResume();
+
+ fragment.onPause();
+ fragment.detachFromFlutterEngine();
+ verify(mockDelegate, times(1)).onPause();
+ verify(mockDelegate, times(1)).onDestroyView();
+ verify(mockDelegate, times(1)).onDetach();
+
+ fragment.onStop();
+ fragment.onDestroy();
+
+ verify(mockDelegate, never()).onStop();
+ // 1 time same as before.
+ verify(mockDelegate, times(1)).onDestroyView();
+ verify(mockDelegate, times(1)).onDetach();
+ }
}
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEnginePluginRegistryTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java
similarity index 95%
rename from shell/platform/android/test/io/flutter/embedding/engine/FlutterEnginePluginRegistryTest.java
rename to shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java
index ee653348004a1..6037ca8c4e11e 100644
--- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEnginePluginRegistryTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java
@@ -26,7 +26,7 @@
// Run with Robolectric so that Log calls don't crash.
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
-public class FlutterEnginePluginRegistryTest {
+public class FlutterEngineConnectionRegistryTest {
@Test
public void itDoesNotRegisterTheSamePluginTwice() {
Context context = mock(Context.class);
@@ -40,8 +40,8 @@ public void itDoesNotRegisterTheSamePluginTwice() {
FakeFlutterPlugin fakePlugin1 = new FakeFlutterPlugin();
FakeFlutterPlugin fakePlugin2 = new FakeFlutterPlugin();
- FlutterEnginePluginRegistry registry =
- new FlutterEnginePluginRegistry(context, flutterEngine, flutterLoader);
+ FlutterEngineConnectionRegistry registry =
+ new FlutterEngineConnectionRegistry(context, flutterEngine, flutterLoader);
// Verify that the registry doesn't think it contains our plugin yet.
assertFalse(registry.has(fakePlugin1.getClass()));
@@ -80,8 +80,8 @@ public void activityResultListenerCanBeRemovedFromListener() {
AtomicBoolean isFirstCall = new AtomicBoolean(true);
// setup the environment to get the required internal data
- FlutterEnginePluginRegistry registry =
- new FlutterEnginePluginRegistry(context, flutterEngine, flutterLoader);
+ FlutterEngineConnectionRegistry registry =
+ new FlutterEngineConnectionRegistry(context, flutterEngine, flutterLoader);
FakeActivityAwareFlutterPlugin fakePlugin = new FakeActivityAwareFlutterPlugin();
registry.add(fakePlugin);
registry.attachToActivity(activity, lifecycle);