diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index bfd6f47d5071d..7cc9c05902f1f 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -416,6 +416,7 @@ action("robolectric_tests") { "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", "test/io/flutter/embedding/android/FlutterActivityTest.java", + "test/io/flutter/embedding/android/FlutterAndroidComponentTest.java", "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/android/FlutterViewTest.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index a2f13160eb010..a478c053bd40f 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -399,6 +399,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { delegate = new FlutterActivityAndFragmentDelegate(this); delegate.onAttach(this); + delegate.onActivityCreated(savedInstanceState); configureWindowForTransparency(); setContentView(createFlutterView()); @@ -557,6 +558,12 @@ protected void onStop() { lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + delegate.onSaveInstanceState(outState); + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 9176ad6f5486d..2895ba19c470e 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -142,8 +142,6 @@ FlutterEngine getFlutterEngine() { void onAttach(@NonNull Context context) { ensureAlive(); - initializeFlutter(context); - // When "retain instance" is true, the FlutterEngine will survive configuration // changes. Therefore, we create a new one only if one does not already exist. if (flutterEngine == null) { @@ -178,13 +176,6 @@ void onAttach(@NonNull Context context) { host.configureFlutterEngine(flutterEngine); } - private void initializeFlutter(@NonNull Context context) { - FlutterMain.ensureInitializationComplete( - context.getApplicationContext(), - host.getFlutterShellArgs().toArray() - ); - } - /** * Obtains a reference to a FlutterEngine to back this delegate and its {@code host}. *

@@ -223,7 +214,7 @@ private void setupFlutterEngine() { // FlutterView. Log.d(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for" + " this FlutterFragment."); - flutterEngine = new FlutterEngine(host.getContext()); + flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray()); isFlutterEngineFromHost = false; } @@ -257,6 +248,15 @@ View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nulla return flutterSplashView; } + void onActivityCreated(@Nullable Bundle bundle) { + Log.v(TAG, "onActivityCreated. Giving plugins an opportunity to restore state."); + ensureAlive(); + + if (host.shouldAttachEngineToActivity()) { + flutterEngine.getActivityControlSurface().onRestoreInstanceState(bundle); + } + } + /** * Invoke this from {@code Activity#onStart()} or {@code Fragment#onStart()}. *

@@ -404,6 +404,15 @@ void onDestroyView() { flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener); } + void onSaveInstanceState(@Nullable Bundle bundle) { + Log.v(TAG, "onSaveInstanceState. Giving plugins an opportunity to save state."); + ensureAlive(); + + if (host.shouldAttachEngineToActivity()) { + flutterEngine.getActivityControlSurface().onSaveInstanceState(bundle); + } + } + /** * Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDetach()}. *

diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 6ad1037e3f4e0..1cc9d892a4f0d 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -586,6 +586,12 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, return delegate.onCreateView(inflater, container, savedInstanceState); } + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + delegate.onActivityCreated(savedInstanceState); + } + @Override public void onStart() { super.onStart(); @@ -622,6 +628,12 @@ public void onDestroyView() { delegate.onDestroyView(); } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + delegate.onSaveInstanceState(outState); + } + @Override public void onDetach() { super.onDetach(); diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 5a4ec289ccf2f..6a55007a32375 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -8,6 +8,7 @@ import android.arch.lifecycle.LifecycleOwner; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.util.HashSet; import java.util.Set; @@ -143,7 +144,29 @@ public void onPreEngineRestart() { * and {@link FlutterLoader#ensureInitializationComplete(Context, String[])}. */ public FlutterEngine(@NonNull Context context) { - this(context, FlutterLoader.getInstance(), new FlutterJNI()); + this(context, null); + } + + /** + * Same as {@link #FlutterEngine(Context)} with added support for passing Dart + * VM arguments. + *

+ * If the Dart VM has already started, the given arguments will have no effect. + */ + public FlutterEngine(@NonNull Context context, @Nullable String[] dartVmArgs) { + this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs); + } + + /** + * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[])} but with no Dart + * VM flags. + */ + public FlutterEngine( + @NonNull Context context, + @NonNull FlutterLoader flutterLoader, + @NonNull FlutterJNI flutterJNI + ) { + this(context, flutterLoader, flutterJNI, null); } /** @@ -151,10 +174,15 @@ public FlutterEngine(@NonNull Context context) { * * {@code flutterJNI} should be a new instance that has never been attached to an engine before. */ - public FlutterEngine(@NonNull Context context, @NonNull FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI) { + public FlutterEngine( + @NonNull Context context, + @NonNull FlutterLoader flutterLoader, + @NonNull FlutterJNI flutterJNI, + @Nullable String[] dartVmArgs + ) { this.flutterJNI = flutterJNI; flutterLoader.startInitialization(context); - flutterLoader.ensureInitializationComplete(context, null); + flutterLoader.ensureInitializationComplete(context, dartVmArgs); flutterJNI.addEngineLifecycleListener(engineLifecycleListener); attachToJni(); diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java index 173aa915e663c..efa756acc313b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java @@ -11,6 +11,7 @@ import android.content.ContentProvider; import android.content.Context; import android.content.Intent; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -49,6 +50,8 @@ class FlutterEnginePluginRegistry implements PluginRegistry, // Standard FlutterPlugin @NonNull + private final FlutterEngine flutterEngine; + @NonNull private final FlutterPlugin.FlutterPluginBinding pluginBinding; @NonNull private final FlutterEngineAndroidLifecycle flutterEngineAndroidLifecycle; @@ -91,10 +94,14 @@ class FlutterEnginePluginRegistry implements PluginRegistry, @NonNull FlutterEngine flutterEngine, @NonNull FlutterEngineAndroidLifecycle lifecycle ) { + this.flutterEngine = flutterEngine; flutterEngineAndroidLifecycle = lifecycle; pluginBinding = new FlutterPlugin.FlutterPluginBinding( appContext, flutterEngine, + flutterEngine.getDartExecutor(), + flutterEngine.getRenderer(), + flutterEngine.getPlatformViewsController().getRegistry(), lifecycle ); } @@ -288,16 +295,16 @@ public void attachToActivity( detachFromAndroidComponent(); this.activity = activity; - this.activityPluginBinding = new FlutterEngineActivityPluginBinding(activity); + this.activityPluginBinding = new FlutterEngineActivityPluginBinding(activity, lifecycle); this.flutterEngineAndroidLifecycle.setBackingLifecycle(lifecycle); // Activate the PlatformViewsController. This must happen before any plugins attempt // to use it, otherwise an error stack trace will appear that says there is no // flutter/platform_views channel. - pluginBinding.getFlutterEngine().getPlatformViewsController().attach( + flutterEngine.getPlatformViewsController().attach( activity, - pluginBinding.getFlutterEngine().getRenderer(), - pluginBinding.getFlutterEngine().getDartExecutor() + flutterEngine.getRenderer(), + flutterEngine.getDartExecutor() ); // Notify all ActivityAware plugins that they are now attached to a new Activity. @@ -322,7 +329,7 @@ public void detachFromActivityForConfigChanges() { } // Deactivate PlatformViewsController. - pluginBinding.getFlutterEngine().getPlatformViewsController().detach(); + flutterEngine.getPlatformViewsController().detach(); flutterEngineAndroidLifecycle.setBackingLifecycle(null); activity = null; @@ -341,7 +348,7 @@ public void detachFromActivity() { } // Deactivate PlatformViewsController. - pluginBinding.getFlutterEngine().getPlatformViewsController().detach(); + flutterEngine.getPlatformViewsController().detach(); flutterEngineAndroidLifecycle.setBackingLifecycle(null); activity = null; @@ -392,6 +399,26 @@ public void onUserLeaveHint() { Log.e(TAG, "Attempted to notify ActivityAware plugins of onUserLeaveHint, but no Activity was attached."); } } + + @Override + public void onSaveInstanceState(@NonNull Bundle bundle) { + Log.v(TAG, "Forwarding onSaveInstanceState() to plugins."); + if (isAttachedToActivity()) { + activityPluginBinding.onSaveInstanceState(bundle); + } else { + Log.e(TAG, "Attempted to notify ActivityAware plugins of onSaveInstanceState, but no Activity was attached."); + } + } + + @Override + public void onRestoreInstanceState(@Nullable Bundle bundle) { + Log.v(TAG, "Forwarding onRestoreInstanceState() to plugins."); + if (isAttachedToActivity()) { + activityPluginBinding.onRestoreInstanceState(bundle); + } else { + Log.e(TAG, "Attempted to notify ActivityAware plugins of onRestoreInstanceState, but no Activity was attached."); + } + } //------- End ActivityControlSurface ----- //----- Start ServiceControlSurface ---- @@ -400,13 +427,13 @@ private boolean isAttachedToService() { } @Override - public void attachToService(@NonNull Service service, @NonNull Lifecycle lifecycle, boolean isForeground) { + 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(); this.service = service; - this.servicePluginBinding = new FlutterEngineServicePluginBinding(service); + this.servicePluginBinding = new FlutterEngineServicePluginBinding(service, lifecycle); flutterEngineAndroidLifecycle.setBackingLifecycle(lifecycle); // Notify all ServiceAware plugins that they are now attached to a new Service. @@ -523,6 +550,8 @@ private static class FlutterEngineActivityPluginBinding implements ActivityPlugi @NonNull private final Activity activity; @NonNull + private final Lifecycle lifecycle; + @NonNull private final Set onRequestPermissionsResultListeners = new HashSet<>(); @NonNull private final Set onActivityResultListeners = new HashSet<>(); @@ -530,9 +559,12 @@ private static class FlutterEngineActivityPluginBinding implements ActivityPlugi private final Set onNewIntentListeners = new HashSet<>(); @NonNull private final Set onUserLeaveHintListeners = new HashSet<>(); + @NonNull + private final Set onSaveInstanceStateListeners = new HashSet<>(); - public FlutterEngineActivityPluginBinding(@NonNull Activity activity) { + public FlutterEngineActivityPluginBinding(@NonNull Activity activity, @NonNull Lifecycle lifecycle) { this.activity = activity; + this.lifecycle = lifecycle; } @Override @@ -541,6 +573,12 @@ public Activity getActivity() { return activity; } + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + @Override public void addRequestPermissionsResultListener(@NonNull io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener listener) { onRequestPermissionsResultListeners.add(listener); @@ -615,6 +653,16 @@ public void removeOnUserLeaveHintListener(@NonNull io.flutter.plugin.common.Plug onUserLeaveHintListeners.remove(listener); } + @Override + public void addOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener) { + onSaveInstanceStateListeners.add(listener); + } + + @Override + public void removeOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener) { + onSaveInstanceStateListeners.remove(listener); + } + /** * Invoked by the {@link FlutterEngine} that owns this {@code ActivityPluginBinding} when its * associated {@link Activity} has its {@code onUserLeaveHint()} method invoked. @@ -624,16 +672,41 @@ void onUserLeaveHint() { listener.onUserLeaveHint(); } } + + /** + * Invoked by the {@link FlutterEngine} that owns this {@code ActivityPluginBinding} when its + * associated {@link Activity} or {@code Fragment} has its {@code onSaveInstanceState(Bundle)} + * method invoked. + */ + void onSaveInstanceState(@NonNull Bundle bundle) { + for (OnSaveInstanceStateListener listener : onSaveInstanceStateListeners) { + listener.onSaveInstanceState(bundle); + } + } + + /** + * Invoked by the {@link FlutterEngine} that owns this {@code ActivityPluginBinding} when its + * associated {@link Activity} has its {@code onCreate(Bundle)} method invoked, or its + * associated {@code Fragment} has its {@code onActivityCreated(Bundle)} method invoked. + */ + void onRestoreInstanceState(@Nullable Bundle bundle) { + for (OnSaveInstanceStateListener listener : onSaveInstanceStateListeners) { + listener.onRestoreInstanceState(bundle); + } + } } private static class FlutterEngineServicePluginBinding implements ServicePluginBinding { @NonNull private final Service service; + @Nullable + private final Lifecycle lifecycle; @NonNull private final Set onModeChangeListeners = new HashSet<>(); - FlutterEngineServicePluginBinding(@NonNull Service service) { + FlutterEngineServicePluginBinding(@NonNull Service service, @Nullable Lifecycle lifecycle) { this.service = service; + this.lifecycle = lifecycle; } @Override @@ -642,6 +715,12 @@ public Service getService() { return service; } + @Nullable + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + @Override public void addOnModeChangeListener(@NonNull ServiceAware.OnModeChangeListener listener) { onModeChangeListeners.add(listener); diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java b/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java index 3202d15dd45c8..eaefdea884409 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java @@ -10,6 +10,9 @@ import android.support.annotation.NonNull; import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.view.TextureRegistry; /** * Interface to be implemented by all Flutter plugins. @@ -41,13 +44,8 @@ * Do not access any properties of the {@link FlutterPluginBinding} after the completion of * {@link #onDetachedFromEngine(FlutterPluginBinding)}. *

- * The Android side of a Flutter plugin can be thought of as applying itself to a {@link FlutterEngine}. - * Use {@link FlutterPluginBinding#getFlutterEngine()} to retrieve the {@link FlutterEngine} that - * the {@code FlutterPlugin} is attached to. To register a - * {@link io.flutter.plugin.common.MethodChannel}, obtain the {@link FlutterEngine}'s - * {@link io.flutter.embedding.engine.dart.DartExecutor} with {@link FlutterEngine#getDartExecutor()}, - * then pass the {@link io.flutter.embedding.engine.dart.DartExecutor} to the - * {@link io.flutter.plugin.common.MethodChannel} as a {@link io.flutter.plugin.common.BinaryMessenger}. + * To register a {@link io.flutter.plugin.common.MethodChannel}, obtain a {@link BinaryMessenger} + * via the {@link FlutterPluginBinding}. *

* An Android Flutter plugin may require access to app resources or other artifacts that can only * be retrieved through a {@link Context}. Developers can access the application context via @@ -81,30 +79,34 @@ public interface FlutterPlugin { /** * Resources made available to all plugins registered with a given {@link FlutterEngine}. *

- * The {@code FlutterPluginBinding}'s {@code flutterEngine} refers to the {@link FlutterEngine} - * that the associated {@code FlutterPlugin} is intended to apply to. For example, if a - * {@code FlutterPlugin} needs to setup a communication channel with its associated Flutter app, - * that can be done by wrapping a {@code MethodChannel} around - * {@link FlutterEngine#getDartExecutor()}. + * The provided {@link BinaryMessenger} can be used to communicate with Dart code running + * in the Flutter context associated with this plugin binding. *

- * A {@link FlutterEngine} may move from foreground to background, from an {@code Activity} to - * a {@code Service}. {@code FlutterPluginBinding}'s {@code lifecycle} generalizes those - * lifecycles so that a {@code FlutterPlugin} can react to lifecycle events without being - * concerned about which Android Component is currently holding the {@link FlutterEngine}. - * TODO(mattcarroll): add info about ActivityAware and ServiceAware for plugins that care. + * Plugins that need to respond to {@code Lifecycle} events should implement the additional + * {@link ActivityAware} and/or {@link ServiceAware} interfaces, where a {@link Lifecycle} + * reference can be obtained. */ class FlutterPluginBinding implements LifecycleOwner { private final Context applicationContext; private final FlutterEngine flutterEngine; + private final BinaryMessenger binaryMessenger; + private final TextureRegistry textureRegistry; + private final PlatformViewRegistry platformViewRegistry; private final Lifecycle lifecycle; public FlutterPluginBinding( @NonNull Context applicationContext, @NonNull FlutterEngine flutterEngine, + @NonNull BinaryMessenger binaryMessenger, + @NonNull TextureRegistry textureRegistry, + @NonNull PlatformViewRegistry platformViewRegistry, @NonNull Lifecycle lifecycle ) { this.applicationContext = applicationContext; this.flutterEngine = flutterEngine; + this.binaryMessenger = binaryMessenger; + this.textureRegistry = textureRegistry; + this.platformViewRegistry = platformViewRegistry; this.lifecycle = lifecycle; } @@ -113,11 +115,37 @@ public Context getApplicationContext() { return applicationContext; } + /** + * @deprecated + * Use {@code getBinaryMessenger()}, {@code getTextureRegistry()}, or + * {@code getPlatformViewRegistry()} instead. + */ + @Deprecated @NonNull public FlutterEngine getFlutterEngine() { return flutterEngine; } + @NonNull + public BinaryMessenger getBinaryMessenger() { + return binaryMessenger; + } + + @NonNull + public TextureRegistry getTextureRegistry() { + return textureRegistry; + } + + @NonNull + public PlatformViewRegistry getPlatformViewRegistry() { + return platformViewRegistry; + } + + /** + * @deprecated + * Use ActivityPluginBinding Lifecycle or ServicePluginBinding Lifecycle instead. + */ + @Deprecated @Override @NonNull public Lifecycle getLifecycle() { 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 0f51c78d9595f..432f2e278f3b8 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 @@ -7,6 +7,7 @@ import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.content.Intent; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -98,4 +99,19 @@ public interface ActivityControlSurface { * {@link FlutterEngine} and the associated method in the {@link Activity} is invoked. */ void onUserLeaveHint(); + + /** + * Call this method from the {@link Activity} or {@code Fragment} that is attached to this + * {@code ActivityControlSurface}'s {@link FlutterEngine} when the associated method is invoked + * in the {@link Activity} or {@code Fragment}. + */ + void onSaveInstanceState(@NonNull Bundle bundle); + + /** + * Call this method from the {@link Activity} or {@code Fragment} that is attached to this + * {@code ActivityControlSurface}'s {@link FlutterEngine} when {@link Activity#onCreate(Bundle)} + * or {@code Fragment#onActivityCreated(Bundle)} is invoked in the {@link Activity} or + * {@code Fragment}. + */ + void onRestoreInstanceState(@Nullable Bundle bundle); } diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java b/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java index 2958d79a81322..7bd8647842e6c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java @@ -5,10 +5,12 @@ package io.flutter.embedding.engine.plugins.activity; import android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.platform.PlatformViewsController; /** * Binding that gives {@link ActivityAware} plugins access to an associated {@link Activity} and @@ -23,6 +25,12 @@ public interface ActivityPluginBinding { @NonNull Activity getActivity(); + /** + * Returns the {@link Lifecycle} associated with the attached {@code Activity}. + */ + @NonNull + Lifecycle getLifecycle(); + /** * Adds a listener that is invoked whenever the associated {@link Activity}'s * {@code onRequestPermissionsResult(...)} method is invoked. @@ -66,4 +74,30 @@ public interface ActivityPluginBinding { * Removes a listener that was added in {@link #addOnUserLeaveHintListener(PluginRegistry.UserLeaveHintListener)}. */ void removeOnUserLeaveHintListener(@NonNull PluginRegistry.UserLeaveHintListener listener); + + /** + * Adds a listener that is invoked when the associated {@code Activity} or {@code Fragment} + * saves and restores instance state. + */ + void addOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener); + + /** + * Removes a listener that was added in {@link #addOnSaveStateListener(OnSaveInstanceStateListener)}. + */ + void removeOnSaveStateListener(@NonNull OnSaveInstanceStateListener listener); + + interface OnSaveInstanceStateListener { + /** + * Invoked when the associated {@code Activity} or {@code Fragment} executes + * {@link Activity#onSaveInstanceState(Bundle)}. + */ + void onSaveInstanceState(@NonNull Bundle bundle); + + /** + * Invoked when the associated {@code Activity} executes + * {@link Activity#onCreate(Bundle)} or associated {@code Fragment} executes + * {@code Fragment#onActivityCreated(Bundle)}. + */ + void onRestoreInstanceState(@Nullable Bundle bundle); + } } diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java b/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java index 82c513d6718c4..352379d88fd3d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java @@ -7,6 +7,7 @@ import android.app.Service; import android.arch.lifecycle.Lifecycle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; /** * Control surface through which a {@link Service} attaches to a {@link FlutterEngine}. @@ -27,7 +28,7 @@ public interface ServiceControlSurface { * {@code isForeground} should be true if the given {@link Service} is running in the foreground, * false otherwise. */ - void attachToService(@NonNull Service service, @NonNull Lifecycle lifecycle, boolean isForeground); + void attachToService(@NonNull Service service, @Nullable Lifecycle lifecycle, boolean isForeground); /** * Call this method from the {@link Service} that is attached to this {@code ServiceControlSurfaces}'s diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java b/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java index 8621bc90a1e81..45a2dbf2f5d58 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java @@ -5,7 +5,9 @@ package io.flutter.embedding.engine.plugins.service; import android.app.Service; +import android.arch.lifecycle.Lifecycle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; /** * Binding that gives {@link ServiceAware} plugins access to an associated {@link Service}. @@ -19,6 +21,12 @@ public interface ServicePluginBinding { @NonNull Service getService(); + /** + * Returns the {@link Lifecycle} associated with the attached {@code Service}. + */ + @Nullable + Lifecycle getLifecycle(); + /** * Adds the given {@code listener} to be notified when the associated {@link Service} goes * from background to foreground, or foreground to background. diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java index 39e5ea5764353..3e88d31144174 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java @@ -64,17 +64,17 @@ public Context activeContext() { @Override public BinaryMessenger messenger() { - return pluginBinding != null ? pluginBinding.getFlutterEngine().getDartExecutor() : null; + return pluginBinding != null ? pluginBinding.getBinaryMessenger() : null; } @Override public TextureRegistry textures() { - return pluginBinding != null ? pluginBinding.getFlutterEngine().getRenderer() : null; + return pluginBinding != null ? pluginBinding.getTextureRegistry() : null; } @Override public PlatformViewRegistry platformViewRegistry() { - return pluginBinding != null ? pluginBinding.getFlutterEngine().getPlatformViewsController().getRegistry() : null; + return pluginBinding != null ? pluginBinding.getPlatformViewRegistry() : null; } @Override diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index b400fb5625e1c..2eae27cb3aa4c 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -9,6 +9,7 @@ import org.junit.runners.Suite.SuiteClasses; import io.flutter.embedding.android.FlutterActivityTest; +import io.flutter.embedding.android.FlutterAndroidComponentTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.android.FlutterViewTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; @@ -23,8 +24,9 @@ @RunWith(Suite.class) @SuiteClasses({ - // FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this + //FlutterActivityAndFragmentDelegateTest.class, //TODO(mklim): Fix and re-enable this FlutterActivityTest.class, + FlutterAndroidComponentTest.class, FlutterEngineCacheTest.class, FlutterFragmentTest.class, FlutterJNITest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java new file mode 100644 index 0000000000000..0104250789be3 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -0,0 +1,273 @@ +package io.flutter.embedding.android; + +import android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +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.annotation.Config; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.loader.FlutterLoader; +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.plugin.platform.PlatformPlugin; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterAndroidComponentTest { + @Test + public void pluginsReceiveFlutterPluginBinding() { + // ---- 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); + + // Add mock plugin. + FlutterPlugin mockPlugin = mock(FlutterPlugin.class); + cachedEngine.getPlugins().add(mockPlugin); + + // Create a fake Host, which is required by the delegate. + FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + + // Verify that the plugin is attached to the FlutterEngine. + ArgumentCaptor pluginBindingCaptor = ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); + verify(mockPlugin, times(1)).onAttachedToEngine(pluginBindingCaptor.capture()); + FlutterPlugin.FlutterPluginBinding binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getApplicationContext()); + assertNotNull(binding.getBinaryMessenger()); + assertNotNull(binding.getTextureRegistry()); + assertNotNull(binding.getPlatformViewRegistry()); + + delegate.onActivityCreated(null); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); + + // Verify the plugin was detached from the FlutterEngine. + pluginBindingCaptor = ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); + verify(mockPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture()); + binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getApplicationContext()); + assertNotNull(binding.getBinaryMessenger()); + assertNotNull(binding.getTextureRegistry()); + assertNotNull(binding.getPlatformViewRegistry()); + } + + @Test + public void activityAwarePluginsReceiveActivityBinding() { + // ---- 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); + + // Add mock plugin. + FlutterPlugin mockPlugin = mock(FlutterPlugin.class, withSettings().extraInterfaces(ActivityAware.class)); + ActivityAware activityAwarePlugin = (ActivityAware) mockPlugin; + ActivityPluginBinding.OnSaveInstanceStateListener mockSaveStateListener = mock(ActivityPluginBinding.OnSaveInstanceStateListener.class); + + // Add a OnSaveStateListener when the Activity plugin binding is made available. + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ActivityPluginBinding binding = (ActivityPluginBinding) invocation.getArguments()[0]; + binding.addOnSaveStateListener(mockSaveStateListener); + return null; + } + }).when(activityAwarePlugin).onAttachedToActivity(any(ActivityPluginBinding.class)); + + cachedEngine.getPlugins().add(mockPlugin); + + // Create a fake Host, which is required by the delegate. + FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine); + + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + + // Verify plugin was given an ActivityPluginBinding. + ArgumentCaptor pluginBindingCaptor = ArgumentCaptor.forClass(ActivityPluginBinding.class); + verify(activityAwarePlugin, times(1)).onAttachedToActivity(pluginBindingCaptor.capture()); + ActivityPluginBinding binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getActivity()); + assertNotNull(binding.getLifecycle()); + + delegate.onActivityCreated(null); + + // Verify that after Activity creation, the plugin was allowed to restore state. + verify(mockSaveStateListener, times(1)).onRestoreInstanceState(any(Bundle.class)); + + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onSaveInstanceState(mock(Bundle.class)); + + // Verify that the plugin was allowed to save state. + verify(mockSaveStateListener, times(1)).onSaveInstanceState(any(Bundle.class)); + + delegate.onDestroyView(); + delegate.onDetach(); + + // Verify that the plugin was detached from the Activity. + verify(activityAwarePlugin, times(1)).onDetachedFromActivity(); + } + + private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host { + final FlutterEngine cachedEngine; + Activity activity; + Lifecycle lifecycle = mock(Lifecycle.class); + + private FakeHost(@NonNull FlutterEngine flutterEngine) { + cachedEngine = flutterEngine; + } + + @NonNull + @Override + public Context getContext() { + return RuntimeEnvironment.application; + } + + + @Nullable + @Override + public Activity getActivity() { + if (activity == null) { + activity = Robolectric.setupActivity(Activity.class); + } + return activity; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + + @NonNull + @Override + public FlutterShellArgs getFlutterShellArgs() { + return new FlutterShellArgs(new String[]{}); + } + + @Nullable + @Override + public String getCachedEngineId() { + return "my_flutter_engine"; + } + + @Override + public boolean shouldDestroyEngineWithHost() { + return true; + } + + @NonNull + @Override + public String getDartEntrypointFunctionName() { + return "main"; + } + + @NonNull + @Override + public String getAppBundlePath() { + return "/fake/path"; + } + + @Nullable + @Override + public String getInitialRoute() { + return "/"; + } + + @NonNull + @Override + public FlutterView.RenderMode getRenderMode() { + return FlutterView.RenderMode.surface; + } + + @NonNull + @Override + public FlutterView.TransparencyMode getTransparencyMode() { + return FlutterView.TransparencyMode.transparent; + } + + @Nullable + @Override + public SplashScreen provideSplashScreen() { + return null; + } + + @Nullable + @Override + public FlutterEngine provideFlutterEngine(@NonNull Context context) { + return cachedEngine; + } + + @Nullable + @Override + public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { + return null; + } + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {} + + @Override + public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {} + + @Override + public boolean shouldAttachEngineToActivity() { + return true; + } + + @Override + public void onFlutterUiDisplayed() {} + + @Override + public void onFlutterUiNoLongerDisplayed() {} + } +}