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 @@ * *

    *
  1. 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}. *
  2. 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 @@ *
  3. 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()}. + *
  4. 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);