diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 2770d2953015b..c9818556ae409 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -736,6 +736,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index f68249194b751..a14e3809d22d8 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -184,6 +184,7 @@ android_java_sources = [ "io/flutter/embedding/engine/systemchannels/NavigationChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java", + "io/flutter/embedding/engine/systemchannels/RestorationChannel.java", "io/flutter/embedding/engine/systemchannels/SettingsChannel.java", "io/flutter/embedding/engine/systemchannels/SystemChannel.java", "io/flutter/embedding/engine/systemchannels/TextInputChannel.java", @@ -443,6 +444,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", + "test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java", "test/io/flutter/external/FlutterLaunchTests.java", "test/io/flutter/plugin/common/StandardMessageCodecTest.java", "test/io/flutter/plugin/common/StandardMethodCodecTest.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 2237f304f8277..acc208a8dc7a3 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -11,6 +11,7 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY; @@ -63,6 +64,7 @@ *
  • Chooses Flutter's initial route. *
  • Renders {@code Activity} transparently, if desired. *
  • Offers hooks for subclasses to provide and configure a {@link FlutterEngine}. + *
  • Save and restore instance state, see {@code #shouldRestoreAndSaveState()}; * * *

    Dart entrypoint, initial route, and app bundle path @@ -949,6 +951,18 @@ public void onFlutterUiNoLongerDisplayed() { // no-op } + @Override + public boolean shouldRestoreAndSaveState() { + if (getIntent().hasExtra(EXTRA_ENABLE_STATE_RESTORATION)) { + return getIntent().getBooleanExtra(EXTRA_ENABLE_STATE_RESTORATION, false); + } + if (getCachedEngineId() != null) { + // Prevent overwriting the existing state in a cached engine with restoration state. + return false; + } + return true; + } + /** * Registers all plugins that an app lists in its pubspec.yaml. * diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 93ff3e7165e0c..358ab6c504805 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -63,6 +63,8 @@ */ /* package */ final class FlutterActivityAndFragmentDelegate { private static final String TAG = "FlutterActivityAndFragmentDelegate"; + private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework"; + private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins"; // The FlutterActivity or FlutterFragment that is delegating most of its calls // to this FlutterActivityAndFragmentDelegate. @@ -227,7 +229,8 @@ void onAttach(@NonNull Context context) { new FlutterEngine( host.getContext(), host.getFlutterShellArgs().toArray(), - /*automaticallyRegisterPlugins=*/ false); + /*automaticallyRegisterPlugins=*/ false, + /*willProvideRestorationData=*/ host.shouldRestoreAndSaveState()); isFlutterEngineFromHost = false; } @@ -293,11 +296,22 @@ View onCreateView( } void onActivityCreated(@Nullable Bundle bundle) { - Log.v(TAG, "onActivityCreated. Giving plugins an opportunity to restore state."); + Log.v(TAG, "onActivityCreated. Giving framework and plugins an opportunity to restore state."); ensureAlive(); + Bundle pluginState = null; + byte[] frameworkState = null; + if (bundle != null) { + pluginState = bundle.getBundle(PLUGINS_RESTORATION_BUNDLE_KEY); + frameworkState = bundle.getByteArray(FRAMEWORK_RESTORATION_BUNDLE_KEY); + } + + if (host.shouldRestoreAndSaveState()) { + flutterEngine.getRestorationChannel().setRestorationData(frameworkState); + } + if (host.shouldAttachEngineToActivity()) { - flutterEngine.getActivityControlSurface().onRestoreInstanceState(bundle); + flutterEngine.getActivityControlSurface().onRestoreInstanceState(pluginState); } } @@ -444,11 +458,19 @@ void onDestroyView() { } void onSaveInstanceState(@Nullable Bundle bundle) { - Log.v(TAG, "onSaveInstanceState. Giving plugins an opportunity to save state."); + Log.v(TAG, "onSaveInstanceState. Giving framework and plugins an opportunity to save state."); ensureAlive(); + if (host.shouldRestoreAndSaveState()) { + bundle.putByteArray( + FRAMEWORK_RESTORATION_BUNDLE_KEY, + flutterEngine.getRestorationChannel().getRestorationData()); + } + if (host.shouldAttachEngineToActivity()) { - flutterEngine.getActivityControlSurface().onSaveInstanceState(bundle); + final Bundle plugins = new Bundle(); + flutterEngine.getActivityControlSurface().onSaveInstanceState(plugins); + bundle.putBundle(PLUGINS_RESTORATION_BUNDLE_KEY, plugins); } } @@ -804,5 +826,17 @@ PlatformPlugin providePlatformPlugin( /** Invoked by this delegate when its {@link FlutterView} stops painting pixels. */ void onFlutterUiNoLongerDisplayed(); + + /** + * Whether state restoration is enabled. + * + *

    When this returns true, the instance state provided to {@code onActivityCreated(Bundle)} + * will be forwarded to the framework via the {@code RestorationChannel} and during {@code + * onSaveInstanceState(Bundle)} the current framework instance state obtained from {@code + * RestorationChannel} will be stored in the provided bundle. + * + *

    This defaults to true, unless a cached engine is used. + */ + boolean shouldRestoreAndSaveState(); } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java index d0ff418c742bc..ae7a4a48e67a8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java @@ -23,6 +23,7 @@ public class FlutterActivityLaunchConfigs { /* package */ static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; /* package */ static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; + /* package */ static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration"; // Default configuration. /* package */ static final String DEFAULT_DART_ENTRYPOINT = "main"; diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 97ae4a25254ee..a0d1120360afe 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -113,6 +113,11 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm * outlive the {@code FlutterFragment}. */ protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment"; + /** + * True if the framework state in the engine attached to this engine should be stored and restored + * when this fragment is created and destroyed. + */ + protected static final String ARG_ENABLE_STATE_RESTORATION = "enable_state_restoration"; /** * Creates a {@code FlutterFragment} with a default configuration. @@ -1018,6 +1023,17 @@ public void onFlutterUiNoLongerDisplayed() { } } + @Override + public boolean shouldRestoreAndSaveState() { + if (getArguments().containsKey(ARG_ENABLE_STATE_RESTORATION)) { + return getArguments().getBoolean(ARG_ENABLE_STATE_RESTORATION); + } + if (getCachedEngineId() != null) { + 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/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 3b007172b80ad..053e9b92a1eb9 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -24,6 +24,7 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.RestorationChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -80,6 +81,7 @@ public class FlutterEngine { @NonNull private final LocalizationChannel localizationChannel; @NonNull private final MouseCursorChannel mouseCursorChannel; @NonNull private final NavigationChannel navigationChannel; + @NonNull private final RestorationChannel restorationChannel; @NonNull private final PlatformChannel platformChannel; @NonNull private final SettingsChannel settingsChannel; @NonNull private final SystemChannel systemChannel; @@ -102,6 +104,7 @@ public void onPreEngineRestart() { } platformViewsController.onPreEngineRestart(); + restorationChannel.clearData(); } }; @@ -159,6 +162,39 @@ public FlutterEngine( automaticallyRegisterPlugins); } + /** + * Same as {@link #FlutterEngine(Context, String[], boolean)} with added support for configuring + * whether the engine will receive restoration data. + * + *

    The {@code waitForRestorationData} flag controls whether the engine delays responding to + * requests from the framework for restoration data until that data has been provided to the + * engine via {@code RestorationChannel.setRestorationData(byte[] data)}. If the flag is false, + * the framework may temporarily initialize itself to default values before the restoration data + * has been made available to the engine. Setting {@code waitForRestorationData} to true avoids + * this extra work by delaying initialization until the data is available. + * + *

    When {@code waitForRestorationData} is set, {@code + * RestorationChannel.setRestorationData(byte[] data)} must be called at a later point in time. If + * it later turns out that no restoration data is available to restore the framework from, that + * method must still be called with null as an argument to indicate "no data". + * + *

    If the framework never requests the restoration data, this flag has no effect. + */ + public FlutterEngine( + @NonNull Context context, + @Nullable String[] dartVmArgs, + boolean automaticallyRegisterPlugins, + boolean waitForRestorationData) { + this( + context, + FlutterLoader.getInstance(), + new FlutterJNI(), + new PlatformViewsController(), + dartVmArgs, + automaticallyRegisterPlugins, + waitForRestorationData); + } + /** * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[])} but with no Dart * VM flags. @@ -194,7 +230,10 @@ public FlutterEngine( automaticallyRegisterPlugins); } - /** Fully configurable {@code FlutterEngine} constructor. */ + /** + * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[], boolean)}, plus the + * ability to provide a custom {@code PlatformViewsController}. + */ public FlutterEngine( @NonNull Context context, @NonNull FlutterLoader flutterLoader, @@ -202,6 +241,25 @@ public FlutterEngine( @NonNull PlatformViewsController platformViewsController, @Nullable String[] dartVmArgs, boolean automaticallyRegisterPlugins) { + this( + context, + flutterLoader, + flutterJNI, + platformViewsController, + dartVmArgs, + automaticallyRegisterPlugins, + false); + } + + /** Fully configurable {@code FlutterEngine} constructor. */ + public FlutterEngine( + @NonNull Context context, + @NonNull FlutterLoader flutterLoader, + @NonNull FlutterJNI flutterJNI, + @NonNull PlatformViewsController platformViewsController, + @Nullable String[] dartVmArgs, + boolean automaticallyRegisterPlugins, + boolean waitForRestorationData) { this.flutterJNI = flutterJNI; flutterLoader.startInitialization(context.getApplicationContext()); flutterLoader.ensureInitializationComplete(context, dartVmArgs); @@ -224,6 +282,7 @@ public FlutterEngine( mouseCursorChannel = new MouseCursorChannel(dartExecutor); navigationChannel = new NavigationChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); + restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData); settingsChannel = new SettingsChannel(dartExecutor); systemChannel = new SystemChannel(dartExecutor); textInputChannel = new TextInputChannel(dartExecutor); @@ -380,6 +439,18 @@ public PlatformChannel getPlatformChannel() { return platformChannel; } + /** + * System channel to exchange restoration data between framework and engine. + * + *

    The engine can obtain the current restoration data from the framework via this channel to + * store it on disk and - when the app is relaunched - provide the stored data back to the + * framework to recreate the original state of the app. + */ + @NonNull + public RestorationChannel getRestorationChannel() { + return restorationChannel; + } + /** * System channel that sends platform/user settings from Android to Flutter, e.g., time format, * scale factor, etc. diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java new file mode 100644 index 0000000000000..5e4a57ef41b97 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java @@ -0,0 +1,160 @@ +// 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.engine.systemchannels; + +import android.support.annotation.NonNull; +import io.flutter.Log; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; + +/** + * System channel to exchange restoration data between framework and engine. + * + *

    The engine can obtain the current restoration data from the framework via this channel to + * store it on disk and - when the app is relaunched - provide the stored data back to the framework + * to recreate the original state of the app. + * + *

    The channel can be configured to delay responding to the framework's request for restoration + * data via {@code waitForRestorationData} until the engine-side has provided the data. This is + * useful when the engine is pre-warmed at a point in the application's life cycle where the + * restoration data is not available yet. For example, if the engine is pre-warmed as part of the + * Application before an Activity is created, this flag should be set to true because Android will + * only provide the restoration data to the Activity during the onCreate callback. + * + *

    The current restoration data provided by the framework can be read via {@code + * getRestorationData()}. + */ +public class RestorationChannel { + private static final String TAG = "RestorationChannel"; + + public RestorationChannel( + @NonNull DartExecutor dartExecutor, @NonNull boolean waitForRestorationData) { + this( + new MethodChannel(dartExecutor, "flutter/restoration", StandardMethodCodec.INSTANCE), + waitForRestorationData); + } + + RestorationChannel(MethodChannel channel, @NonNull boolean waitForRestorationData) { + this.channel = channel; + this.waitForRestorationData = waitForRestorationData; + + channel.setMethodCallHandler(handler); + } + + /** + * Whether the channel delays responding to the framework's initial request for restoration data + * until {@code setRestorationData} has been called. + * + *

    If the engine never calls {@code setRestorationData} this flag must be set to false. If set + * to true, the engine must call {@code setRestorationData} either with the actual restoration + * data as argument or null if it turns out that there is no restoration data. + * + *

    If the response to the framework's request for restoration data is not delayed until the + * data has been set via {@code setRestorationData}, the framework may intermittently initialize + * itself to default values until the restoration data has been made available. Setting this flag + * to true avoids that extra work. + */ + public final boolean waitForRestorationData; + + // Holds the the most current restoration data which may have been provided by the engine + // via "setRestorationData" or by the framework via the method channel. This is the data the + // framework should be restored to in case the app is terminated. + private byte[] restorationData; + private MethodChannel channel; + private MethodChannel.Result pendingFrameworkRestorationChannelRequest; + private boolean engineHasProvidedData = false; + private boolean frameworkHasRequestedData = false; + + /** Obtain the most current restoration data that the framework has provided. */ + public byte[] getRestorationData() { + return restorationData; + } + + /** Set the restoration data from which the framework will restore its state. */ + public void setRestorationData(byte[] data) { + engineHasProvidedData = true; + if (pendingFrameworkRestorationChannelRequest != null) { + // If their is a pending request from the framework, answer it. + pendingFrameworkRestorationChannelRequest.success(data); + pendingFrameworkRestorationChannelRequest = null; + restorationData = data; + } else if (frameworkHasRequestedData) { + // If the framework has previously received the engine's restoration data, push the new data + // directly to it. This case can happen when "waitForRestorationData" is false and the + // framework retrieved the restoration state before it was set via this method. + // Experimentally, this can also be used to restore a previously used engine to another state, + // e.g. when the engine is attached to a new activity. + channel.invokeMethod( + "push", + data, + new MethodChannel.Result() { + @Override + public void success(Object result) { + restorationData = data; + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + Log.e( + TAG, + "Error " + + errorCode + + " while sending restoration data to framework: " + + errorMessage); + } + + @Override + public void notImplemented() { + // Nothing to do. + } + }); + } else { + // Otherwise, just cache the data until the framework asks for it. + restorationData = data; + } + } + + /** + * Clears the current restoration data. + * + *

    This should be called just prior to a hot restart. Otherwise, after the hot restart the + * state prior to the hot restart will get restored. + */ + public void clearData() { + restorationData = null; + } + + private final MethodChannel.MethodCallHandler handler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final String method = call.method; + final Object args = call.arguments; + switch (method) { + case "put": + restorationData = (byte[]) args; + result.success(null); + break; + case "get": + frameworkHasRequestedData = true; + if (engineHasProvidedData || !waitForRestorationData) { + result.success(restorationData); + // Do not delete the restoration data on the engine side after sending it to the + // framework. We may need to hand this data back to the operating system if the + // framework never modifies the data (and thus doesn't send us any + // data back). + } else { + pendingFrameworkRestorationChannelRequest = result; + } + break; + default: + result.notImplemented(); + break; + } + } + }; +} diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index f8cbb2c71090b..41dbae9c9f8dd 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -26,7 +26,7 @@ *

    The logical identity of the channel is given by its name. Identically named channels will * interfere with each other's communication. */ -public final class MethodChannel { +public class MethodChannel { private static final String TAG = "MethodChannel#"; private final BinaryMessenger messenger; diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 7a101deb2f9bc..7df51ee9dba62 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -16,6 +16,7 @@ import io.flutter.embedding.engine.RenderingComponentTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; +import io.flutter.embedding.engine.systemchannels.RestorationChannelTest; import io.flutter.external.FlutterLaunchTests; import io.flutter.plugin.common.StandardMessageCodecTest; import io.flutter.plugin.common.StandardMethodCodecTest; @@ -63,6 +64,7 @@ TextInputPluginTest.class, MouseCursorPluginTest.class, AccessibilityBridgeTest.class, + RestorationChannelTest.class, }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ public class FlutterTestSuite {} 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 66d8f5dc0135b..9255cb4077b67 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -272,6 +272,11 @@ public boolean shouldAttachEngineToActivity() { return true; } + @Override + public boolean shouldRestoreAndSaveState() { + return true; + } + @Override public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java new file mode 100644 index 0000000000000..c4cffca60122b --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java @@ -0,0 +1,102 @@ +package io.flutter.embedding.engine.systemchannels; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.annotation.TargetApi; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config( + manifest = Config.NONE, + shadows = {}) +@RunWith(RobolectricTestRunner.class) +@TargetApi(24) +public class RestorationChannelTest { + @Test + public void itDoesNotDoAnythingWhenRestorationDataIsSetBeforeFrameworkAsks() + throws JSONException { + MethodChannel rawChannel = mock(MethodChannel.class); + RestorationChannel restorationChannel = + new RestorationChannel(rawChannel, /*waitForRestorationData=*/ false); + restorationChannel.setRestorationData("Any String you want".getBytes()); + verify(rawChannel, times(0)).invokeMethod(any(), any()); + } + + @Test + public void itSendsDataOverWhenRequestIsPending() throws JSONException { + byte[] data = "Any String you want".getBytes(); + + MethodChannel rawChannel = mock(MethodChannel.class); + RestorationChannel restorationChannel = + new RestorationChannel(rawChannel, /*waitForRestorationData=*/ true); + ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(MethodChannel.MethodCallHandler.class); + verify(rawChannel).setMethodCallHandler(argumentCaptor.capture()); + + MethodChannel.Result result = mock(MethodChannel.Result.class); + argumentCaptor.getValue().onMethodCall(new MethodCall("get", null), result); + verifyZeroInteractions(result); + + restorationChannel.setRestorationData(data); + verify(rawChannel, times(0)).invokeMethod(any(), any()); + verify(result).success(data); + + // Next get request is answered right away. + MethodChannel.Result result2 = mock(MethodChannel.Result.class); + argumentCaptor.getValue().onMethodCall(new MethodCall("get", null), result2); + verify(result2).success(data); + } + + @Test + public void itPushesNewData() throws JSONException { + byte[] data = "Any String you want".getBytes(); + + MethodChannel rawChannel = mock(MethodChannel.class); + RestorationChannel restorationChannel = + new RestorationChannel(rawChannel, /*waitForRestorationData=*/ false); + ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(MethodChannel.MethodCallHandler.class); + verify(rawChannel).setMethodCallHandler(argumentCaptor.capture()); + + MethodChannel.Result result = mock(MethodChannel.Result.class); + argumentCaptor.getValue().onMethodCall(new MethodCall("get", null), result); + verify(result).success(null); + + restorationChannel.setRestorationData(data); + assertEquals(restorationChannel.getRestorationData(), null); + + ArgumentCaptor resultCapture = + ArgumentCaptor.forClass(MethodChannel.Result.class); + verify(rawChannel).invokeMethod(eq("push"), eq(data), resultCapture.capture()); + resultCapture.getValue().success(null); + assertEquals(restorationChannel.getRestorationData(), data); + } + + @Test + public void itHoldsOnToDataFromFramework() throws JSONException { + byte[] data = "Any String you want".getBytes(); + + MethodChannel rawChannel = mock(MethodChannel.class); + RestorationChannel restorationChannel = + new RestorationChannel(rawChannel, /*waitForRestorationData=*/ false); + ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(MethodChannel.MethodCallHandler.class); + verify(rawChannel).setMethodCallHandler(argumentCaptor.capture()); + + MethodChannel.Result result = mock(MethodChannel.Result.class); + argumentCaptor.getValue().onMethodCall(new MethodCall("put", data), result); + assertEquals(restorationChannel.getRestorationData(), data); + } +}