diff --git a/DEPS b/DEPS index 1cfd411dd31..c9052f8b70c 100644 --- a/DEPS +++ b/DEPS @@ -474,7 +474,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/robolectric_bundle', - 'version': 'last_updated:2019-07-22@11:16:04-07:00' + 'version': 'last_updated:2019-07-29T15:27:42-0700' } ], 'condition': 'download_android_deps', diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index bc32edb502e..59dffca2ddf 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -551,12 +551,16 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Andro 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/FlutterActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 869b70a3e96..5c151e05db8 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -127,12 +127,16 @@ action("flutter_shell_java") { "io/flutter/embedding/android/AndroidTouchProcessor.java", "io/flutter/embedding/android/DrawableSplashScreen.java", "io/flutter/embedding/android/FlutterActivity.java", + "io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java", + "io/flutter/embedding/android/FlutterEngineConfigurator.java", + "io/flutter/embedding/android/FlutterEngineProvider.java", "io/flutter/embedding/android/FlutterFragment.java", "io/flutter/embedding/android/FlutterSplashView.java", "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", "io/flutter/embedding/android/FlutterView.java", "io/flutter/embedding/android/SplashScreen.java", + "io/flutter/embedding/android/SplashScreenProvider.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java", "io/flutter/embedding/engine/FlutterEnginePluginRegistry.java", @@ -325,6 +329,7 @@ action("robolectric_tests") { sources = [ "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", + "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", "test/io/flutter/util/PreconditionsTest.java", ] @@ -340,7 +345,13 @@ action("robolectric_tests") { "//third_party/robolectric/lib/junit-3.8.jar", "//third_party/robolectric/lib/junit-4.13-beta-3.jar", "//third_party/robolectric/lib/robolectric-3.8.jar", + "//third_party/robolectric/lib/shadows-framework-3.8.jar", "//third_party/robolectric/lib/annotations-3.8.jar", + "//third_party/robolectric/lib/runtime-1.1.1.jar", + "//third_party/robolectric/lib/common-1.1.1.jar", + "//third_party/robolectric/lib/common-java8-1.1.1.jar", + "//third_party/robolectric/lib/support-annotations-28.0.0.jar", + "//third_party/robolectric/lib/mockito-all-1.10.19.jar", ] inputs = _jar_dependencies diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 865ada7c36f..2fa4488b9f4 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -4,12 +4,15 @@ package io.flutter.embedding.android; +import android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LifecycleRegistry; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -17,30 +20,25 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; -import android.util.TypedValue; import android.view.View; -import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.widget.FrameLayout; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.view.FlutterMain; /** * {@code Activity} which displays a fullscreen Flutter UI. *

- * WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE. - * IF YOU USE IT, WE WILL BREAK YOU. - *

* {@code FlutterActivity} is the simplest and most direct way to integrate Flutter within an * Android app. *

+ * Dart entrypoint, initial route, and app bundle path + *

* The Dart entrypoint executed within this {@code Activity} is "main()" by default. The entrypoint * may be specified explicitly by passing the name of the entrypoint method as a {@code String} in * {@link #EXTRA_DART_ENTRYPOINT}, e.g., "myEntrypoint". @@ -49,11 +47,18 @@ import io.flutter.view.FlutterMain; * route may be specified explicitly by passing the name of the route as a {@code String} in * {@link #EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link". *

- * The app bundle path, Dart entrypoint, and initial route can each be controlled in a subclass of + * The Dart entrypoint and initial route can each be controlled using a {@link IntentBuilder} + * via the following methods: + *

+ *

+ * The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of * {@code FlutterActivity} by overriding their respective methods: *

* If Flutter is needed in a location that cannot use an {@code Activity}, consider using @@ -65,6 +70,19 @@ import io.flutter.view.FlutterMain; * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a * {@code Fragment}. *

+ * FlutterActivity responsibilities + *

+ * {@code FlutterActivity} maintains the following responsibilities: + *

+ *

* Launch Screen and Splash Screen *

* {@code FlutterActivity} supports the display of an Android "launch screen" as well as a @@ -116,11 +134,9 @@ import io.flutter.view.FlutterMain; * /> * } */ -// TODO(mattcarroll): explain each call forwarded to Fragment (first requires resolution of PluginRegistry API). -public class FlutterActivity extends FragmentActivity - implements FlutterFragment.FlutterEngineProvider, - FlutterFragment.FlutterEngineConfigurator, - FlutterFragment.SplashScreenProvider { +public class FlutterActivity extends Activity + implements FlutterActivityAndFragmentDelegate.Host, + LifecycleOwner { private static final String TAG = "FlutterActivity"; // Meta-data arguments, processed from manifest XML. @@ -139,13 +155,6 @@ public class FlutterActivity extends FragmentActivity protected static final String DEFAULT_INITIAL_ROUTE = "/"; protected static final String DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque.name(); - // FlutterFragment management. - private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment"; - // TODO(mattcarroll): replace ID with R.id when build system supports R.java - private static final int FRAGMENT_CONTAINER_ID = 609893468; // random number - @Nullable - private FlutterFragment flutterFragment; - /** * Creates an {@link Intent} that launches a {@code FlutterActivity}, which executes * a {@code main()} Dart entrypoint, and displays the "/" route as Flutter's initial route. @@ -174,6 +183,19 @@ public class FlutterActivity extends FragmentActivity private String initialRoute = DEFAULT_INITIAL_ROUTE; private String backgroundMode = DEFAULT_BACKGROUND_MODE; + /** + * Constructor that allows this {@code IntentBuilder} to be used by subclasses of + * {@code FlutterActivity}. + *

+ * Subclasses of {@code FlutterActivity} should provide their own static version of + * {@link #createBuilder()}, which returns an instance of {@code IntentBuilder} + * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, + * e.g.: + *

+ * {@code + * return new IntentBuilder(MyFlutterActivity.class); + * } + */ protected IntentBuilder(@NonNull Class activityClass) { this.activityClass = activityClass; } @@ -232,16 +254,32 @@ public class FlutterActivity extends FragmentActivity } } + // Delegate that runs all lifecycle and OS hook logic that is common between + // FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate + // implementation for details about why it exists. + private FlutterActivityAndFragmentDelegate delegate; + + @NonNull + private LifecycleRegistry lifecycle; + + public FlutterActivity() { + lifecycle = new LifecycleRegistry(this); + } + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { switchLaunchThemeForNormalTheme(); super.onCreate(savedInstanceState); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + + delegate = new FlutterActivityAndFragmentDelegate(this); + delegate.onAttach(this); + configureWindowForTransparency(); - setContentView(createFragmentContainer()); + setContentView(createFlutterView()); configureStatusBarForFullscreenFlutterExperience(); - ensureFlutterFragmentCreated(); } /** @@ -288,31 +326,6 @@ public class FlutterActivity extends FragmentActivity } } - /** - * Extracts a {@link Drawable} from the {@code Activity}'s {@code windowBackground}. - *

- * Returns null if no {@code windowBackground} is set for the activity. - */ - private Drawable getLaunchScreenDrawableFromActivityTheme() { - TypedValue typedValue = new TypedValue(); - if (!getTheme().resolveAttribute( - android.R.attr.windowBackground, - typedValue, - true)) { - return null; - } - if (typedValue.resourceId == 0) { - return null; - } - try { - return getResources().getDrawable(typedValue.resourceId, getTheme()); - } catch (Resources.NotFoundException e) { - Log.e(TAG, "Splash screen requested in AndroidManifest.xml, but no windowBackground" - + " is available in the theme."); - return null; - } - } - @Nullable @Override public SplashScreen provideSplashScreen() { @@ -340,7 +353,11 @@ public class FlutterActivity extends FragmentActivity ); Bundle metadata = activityInfo.metaData; Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null; - return splashScreenId != null ? getResources().getDrawable(splashScreenId, getTheme()) : null; + return splashScreenId != null + ? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP + ? getResources().getDrawable(splashScreenId, getTheme()) + : getResources().getDrawable(splashScreenId) + : null; } catch (PackageManager.NameNotFoundException e) { // This is never expected to happen. return null; @@ -365,6 +382,14 @@ public class FlutterActivity extends FragmentActivity } } + @NonNull + private View createFlutterView() { + return delegate.onCreateView( + null /* inflater */, + null /* container */, + null /* savedInstanceState */); + } + private void configureStatusBarForFullscreenFlutterExperience() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = getWindow(); @@ -374,177 +399,126 @@ public class FlutterActivity extends FragmentActivity } } - /** - * Creates a {@link FrameLayout} with an ID of {@code #FRAGMENT_CONTAINER_ID} that will contain - * the {@link FlutterFragment} displayed by this {@code FlutterActivity}. - *

- * @return the FrameLayout container - */ - @NonNull - private View createFragmentContainer() { - FrameLayout container = new FrameLayout(this); - container.setId(FRAGMENT_CONTAINER_ID); - container.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )); - return container; - } - - /** - * Ensure that a {@link FlutterFragment} is attached to this {@code FlutterActivity}. - *

- * If no {@link FlutterFragment} exists in this {@code FlutterActivity}, then a {@link FlutterFragment} - * is created and added. If a {@link FlutterFragment} does exist in this {@code FlutterActivity}, then - * a reference to that {@link FlutterFragment} is retained in {@code #flutterFragment}. - */ - private void ensureFlutterFragmentCreated() { - FragmentManager fragmentManager = getSupportFragmentManager(); - flutterFragment = (FlutterFragment) fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT); - if (flutterFragment == null) { - // No FlutterFragment exists yet. This must be the initial Activity creation. We will create - // and add a new FlutterFragment to this Activity. - flutterFragment = createFlutterFragment(); - fragmentManager - .beginTransaction() - .add(FRAGMENT_CONTAINER_ID, flutterFragment, TAG_FLUTTER_FRAGMENT) - .commit(); - } - } - - /** - * Creates the instance of the {@link FlutterFragment} that this {@code FlutterActivity} displays. - *

- * Subclasses may override this method to return a specialization of {@link FlutterFragment}. - */ - @NonNull - protected FlutterFragment createFlutterFragment() { - BackgroundMode backgroundMode = getBackgroundMode(); - - Log.d(TAG, "Creating FlutterFragment:\n" - + "Background transparency mode: " + backgroundMode + "\n" - + "Dart entrypoint: " + getDartEntrypoint() + "\n" - + "Initial route: " + getInitialRoute() + "\n" - + "App bundle path: " + getAppBundlePath() + "\n" - + "Will attach FlutterEngine to Activity: " + shouldAttachEngineToActivity()); - - return new FlutterFragment.Builder() - .dartEntrypoint(getDartEntrypoint()) - .initialRoute(getInitialRoute()) - .appBundlePath(getAppBundlePath()) - .flutterShellArgs(FlutterShellArgs.fromIntent(getIntent())) - .renderMode(backgroundMode == BackgroundMode.opaque - ? FlutterView.RenderMode.surface - : FlutterView.RenderMode.texture) - .transparencyMode(backgroundMode == BackgroundMode.opaque - ? FlutterView.TransparencyMode.opaque - : FlutterView.TransparencyMode.transparent) - .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) - .build(); - } - - /** - * Hook for subclasses to control whether or not the {@link FlutterFragment} within this - * {@code Activity} automatically attaches its {@link FlutterEngine} to this {@code Activity}. - *

- * For an explanation of why this control exists, see {@link FlutterFragment.Builder#shouldAttachEngineToActivity()}. - *

- * This property is controlled with a protected method instead of an {@code Intent} argument because - * the only situation where changing this value would help, is a situation in which - * {@code FlutterActivity} is being subclassed to utilize a custom and/or cached {@link FlutterEngine}. - *

- * Defaults to {@code true}. - */ - protected boolean shouldAttachEngineToActivity() { - return true; - } - - /** - * Hook for subclasses to easily provide a custom {@code FlutterEngine}. - */ - @Nullable @Override - public FlutterEngine provideFlutterEngine(@NonNull Context context) { - // No-op. Hook for subclasses. - return null; + protected void onStart() { + super.onStart(); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START); + delegate.onStart(); } - /** - * Hook for subclasses to easily configure a {@code FlutterEngine}, e.g., register - * plugins. - *

- * This method is called after {@link #provideFlutterEngine(Context)}. - */ @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - // No-op. Hook for subclasses. + protected void onResume() { + super.onResume(); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + delegate.onResume(); } @Override public void onPostResume() { super.onPostResume(); - flutterFragment.onPostResume(); + delegate.onPostResume(); + } + + @Override + protected void onPause() { + super.onPause(); + delegate.onPause(); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + } + + @Override + protected void onStop() { + super.onStop(); + delegate.onStop(); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + delegate.onDestroyView(); + delegate.onDetach(); + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + delegate.onActivityResult(requestCode, resultCode, data); } @Override protected void onNewIntent(@NonNull Intent intent) { - // Forward Intents to our FlutterFragment in case it cares. - flutterFragment.onNewIntent(intent); + // TODO(mattcarroll): change G3 lint rule that forces us to call super super.onNewIntent(intent); + delegate.onNewIntent(intent); } @Override public void onBackPressed() { - flutterFragment.onBackPressed(); + delegate.onBackPressed(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - flutterFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + delegate.onRequestPermissionsResult(requestCode, permissions, grantResults); } @Override public void onUserLeaveHint() { - flutterFragment.onUserLeaveHint(); + delegate.onUserLeaveHint(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); - flutterFragment.onTrimMemory(level); - } - - @SuppressWarnings("unused") - @Nullable - protected FlutterEngine getFlutterEngine() { - return flutterFragment.getFlutterEngine(); + delegate.onTrimMemory(level); } /** - * The path to the bundle that contains this Flutter app's resources, e.g., Dart code snapshots. - *

- * When this {@code FlutterActivity} is run by Flutter tooling and a data String is included - * in the launching {@code Intent}, that data String is interpreted as an app bundle path. - *

- * By default, the app bundle path is obtained from {@link FlutterMain#findAppBundlePath(Context)}. - *

- * Subclasses may override this method to return a custom app bundle path. + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain a {@code Context} reference as + * needed. + */ + @Override + @NonNull + public Context getContext() { + return this; + } + + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain an {@code Activity} reference as + * needed. This reference is used by the delegate to instantiate a {@link FlutterView}, + * a {@link PlatformPlugin}, and to determine if the {@code Activity} is changing + * configurations. + */ + @Override + @NonNull + public Activity getActivity() { + return this; + } + + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain a {@code Lifecycle} reference as + * needed. This reference is used by the delegate to provide Flutter plugins with access + * to lifecycle events. + */ + @Override + @NonNull + public Lifecycle getLifecycle() { + return lifecycle; + } + + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when + * initializing Flutter. */ @NonNull - protected String getAppBundlePath() { - // If this Activity was launched from tooling, and the incoming Intent contains - // a custom app bundle path, return that path. - // TODO(mattcarroll): determine if we should have an explicit FlutterTestActivity instead of conflating. - if (isDebuggable() && Intent.ACTION_RUN.equals(getIntent().getAction())) { - String appBundlePath = getIntent().getDataString(); - if (appBundlePath != null) { - return appBundlePath; - } - } - - // Return the default app bundle path. - // TODO(mattcarroll): move app bundle resolution into an appropriately named class. - return FlutterMain.findAppBundlePath(getApplicationContext()); + @Override + public FlutterShellArgs getFlutterShellArgs() { + return FlutterShellArgs.fromIntent(getIntent()); } /** @@ -565,7 +539,7 @@ public class FlutterActivity extends FragmentActivity * Subclasses may override this method to directly control the Dart entrypoint. */ @NonNull - protected String getDartEntrypoint() { + public String getDartEntrypointFunctionName() { if (getIntent().hasExtra(EXTRA_DART_ENTRYPOINT)) { return getIntent().getStringExtra(EXTRA_DART_ENTRYPOINT); } @@ -601,7 +575,7 @@ public class FlutterActivity extends FragmentActivity * Subclasses may override this method to directly control the initial route. */ @NonNull - protected String getInitialRoute() { + public String getInitialRoute() { if (getIntent().hasExtra(EXTRA_INITIAL_ROUTE)) { return getIntent().getStringExtra(EXTRA_INITIAL_ROUTE); } @@ -619,6 +593,69 @@ public class FlutterActivity extends FragmentActivity } } + /** + * The path to the bundle that contains this Flutter app's resources, e.g., Dart code snapshots. + *

+ * When this {@code FlutterActivity} is run by Flutter tooling and a data String is included + * in the launching {@code Intent}, that data String is interpreted as an app bundle path. + *

+ * By default, the app bundle path is obtained from {@link FlutterMain#findAppBundlePath(Context)}. + *

+ * Subclasses may override this method to return a custom app bundle path. + */ + @NonNull + public String getAppBundlePath() { + // If this Activity was launched from tooling, and the incoming Intent contains + // a custom app bundle path, return that path. + // TODO(mattcarroll): determine if we should have an explicit FlutterTestActivity instead of conflating. + if (isDebuggable() && Intent.ACTION_RUN.equals(getIntent().getAction())) { + String appBundlePath = getIntent().getDataString(); + if (appBundlePath != null) { + return appBundlePath; + } + } + + // Return the default app bundle path. + // TODO(mattcarroll): move app bundle resolution into an appropriately named class. + return FlutterMain.findAppBundlePath(getApplicationContext()); + } + + /** + * Returns true if Flutter is running in "debug mode", and false otherwise. + *

+ * Debug mode allows Flutter to operate with hot reload and hot restart. Release mode does not. + */ + private boolean isDebuggable() { + return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain the desired {@link FlutterView.RenderMode} + * that should be used when instantiating a {@link FlutterView}. + */ + @NonNull + @Override + public FlutterView.RenderMode getRenderMode() { + return getBackgroundMode() == BackgroundMode.opaque + ? FlutterView.RenderMode.surface + : FlutterView.RenderMode.texture; + } + + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain the desired + * {@link FlutterView.TransparencyMode} that should be used when instantiating a + * {@link FlutterView}. + */ + @NonNull + @Override + public FlutterView.TransparencyMode getTransparencyMode() { + return getBackgroundMode() == BackgroundMode.opaque + ? FlutterView.TransparencyMode.opaque + : FlutterView.TransparencyMode.transparent; + } + /** * The desired window background mode of this {@code Activity}, which defaults to * {@link BackgroundMode#opaque}. @@ -633,14 +670,101 @@ public class FlutterActivity extends FragmentActivity } /** - * Returns true if Flutter is running in "debug mode", and false otherwise. + * Hook for subclasses to easily provide a custom {@link FlutterEngine}. *

- * Debug mode allows Flutter to operate with hot reload and hot restart. Release mode does not. + * This hook is where a cached {@link FlutterEngine} should be provided, if a cached + * {@link FlutterEngine} is desired. */ - private boolean isDebuggable() { - return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + @Nullable + @Override + public FlutterEngine provideFlutterEngine(@NonNull Context context) { + // No-op. Hook for subclasses. + return null; } + /** + * Hook for subclasses to obtain a reference to the {@link FlutterEngine} that is owned + * by this {@code FlutterActivity}. + */ + @Nullable + protected FlutterEngine getFlutterEngine() { + return delegate.getFlutterEngine(); + } + + @Nullable + @Override + public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { + if (activity != null) { + return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); + } else { + return null; + } + } + + /** + * Hook for subclasses to easily configure a {@code FlutterEngine}, e.g., register + * plugins. + *

+ * This method is called after {@link #provideFlutterEngine(Context)}. + */ + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + // No-op. Hook for subclasses. + } + + /** + * Hook for subclasses to control whether or not the {@link FlutterFragment} within this + * {@code Activity} automatically attaches its {@link FlutterEngine} to this {@code Activity}. + *

+ * This property is controlled with a protected method instead of an {@code Intent} argument because + * the only situation where changing this value would help, is a situation in which + * {@code FlutterActivity} is being subclassed to utilize a custom and/or cached {@link FlutterEngine}. + *

+ * Defaults to {@code true}. + *

+ * Control surfaces are used to provide Android resources and lifecycle events to + * plugins that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity} + * is true then this {@code FlutterActivity} will connect its {@link FlutterEngine} to itself, + * along with any plugins that are registered with that {@link FlutterEngine}. This allows + * plugins to access the {@code Activity}, as well as receive {@code Activity}-specific calls, + * e.g., {@link Activity#onNewIntent(Intent)}. If {@code shouldAttachEngineToActivity} is false, + * then this {@code FlutterActivity} will not automatically manage the connection between its + * {@link FlutterEngine} and itself. In this case, plugins will not be offered a reference to + * an {@code Activity} or its OS hooks. + *

+ * 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 ActivityControlSurface#detachFromActivity()} at the desired times. + *

+ * One reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to move the + * {@link FlutterEngine} somewhere else. For example, a developer might want the + * {@link FlutterEngine} to outlive this {@code FlutterActivity} so that it can be used + * later in a different {@code Activity}. To accomplish this, the {@link FlutterEngine} may + * need to be disconnected from this {@code FluttterActivity} at an unusual time, preventing + * this {@code FlutterActivity} from correctly managing the relationship between the + * {@link FlutterEngine} and itself. + */ + @Override + public boolean shouldAttachEngineToActivity() { + return true; + } + + /** + * Returns true if the {@link FlutterEngine} backing this {@code FlutterActivity} should + * outlive this {@code FlutterActivity}, or be destroyed when the {@code FlutterActivity} + * is destroyed. + */ + @Override + public boolean retainFlutterEngineAfterHostDestruction() { + return false; + } + + @Override + public void onFirstFrameRendered() {} + /** * The mode of the background of a {@code FlutterActivity}, either opaque or transparent. */ diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java new file mode 100644 index 00000000000..c7b3308da38 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -0,0 +1,668 @@ +// 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 android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Arrays; + +import io.flutter.Log; +import io.flutter.app.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; +import io.flutter.plugin.platform.PlatformPlugin; +import io.flutter.view.FlutterMain; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; + +/** + * Delegate that implements all Flutter logic that is the same between a {@link FlutterActivity} + * and a {@link FlutterFragment}. + *

+ * Why does this class exist? + *

+ * One might ask why an {@code Activity} and {@code Fragment} delegate needs to exist. Given + * that a {@code Fragment} can be placed within an {@code Activity}, it would make more sense + * to use a {@link FlutterFragment} within a {@link FlutterActivity}. + *

+ * The {@code Fragment} support library adds 100k of binary size to an app, and full-Flutter + * apps do not otherwise require that binary hit. Therefore, it was concluded that Flutter + * must provide a {@link FlutterActivity} based on the AOSP {@code Activity}, and an independent + * {@link FlutterFragment} for add-to-app developers. + *

+ * If a time ever comes where the inclusion of {@code Fragment}s in a full-Flutter app is no + * longer deemed an issue, this class should be immediately decomposed between + * {@link FlutterActivity} and {@link FlutterFragment} and then eliminated. + *

+ * Caution when modifying this class + *

+ * Any time that a "delegate" is created with the purpose of encapsulating the internal + * behaviors of another object, that delegate is highly susceptible to degeneration. It is + * easy to tack new responsibilities on to the delegate which would not otherwise be added + * to the original object. It is also easy to begin hanging listeners and callbacks on a + * delegate object that likewise would not be added to the original object. A delegate can + * quickly become a complex web of dependencies and optional references that are very + * difficult to track. + *

+ * Maintainers of this class should take care to only place code in this delegate that would + * otherwise be placed in either {@link FlutterActivity} or {@link FlutterFragment}, and in + * exactly the same form. Do not use this class as a convenient shortcut for any other + * behavior. + */ +/* package */ final class FlutterActivityAndFragmentDelegate { + private static final String TAG = "FlutterActivityAndFragmentDelegate"; + + // The FlutterActivity or FlutterFragment that is delegating most of its calls + // to this FlutterActivityAndFragmentDelegate. + @NonNull + private Host host; + @Nullable + private FlutterEngine flutterEngine; + @Nullable + private FlutterSplashView flutterSplashView; + @Nullable + private FlutterView flutterView; + @Nullable + private PlatformPlugin platformPlugin; + private boolean isFlutterEngineFromHost; + + @NonNull + private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { + @Override + public void onFirstFrameRendered() { + host.onFirstFrameRendered(); + } + }; + + FlutterActivityAndFragmentDelegate(@NonNull Host host) { + this.host = host; + } + + /** + * Disconnects this {@code FlutterActivityAndFragmentDelegate} from its host {@code Activity} + * or {@code Fragment}. + *

+ * No further method invocations may occur on this {@code FlutterActivityAndFragmentDelegate} + * after invoking this method. If a method is invoked, an exception will occur. + *

+ * This method only clears out references. It does not destroy its {@link FlutterEngine}. The + * behavior that destroys a {@link FlutterEngine} can be found in {@link #onDetach()}. + */ + void release() { + this.host = null; + this.flutterEngine = null; + this.flutterView = null; + this.platformPlugin = null; + } + + /** + * Returns the {@link FlutterEngine} that is owned by this delegate and its host {@code Activity} + * or {@code Fragment}. + */ + @Nullable + FlutterEngine getFlutterEngine() { + return flutterEngine; + } + + /** + * Invoke this method from {@code Activity#onCreate(Bundle)} or {@code Fragment#onAttach(Context)}. + *

+ * This method does the following: + *

+ *

    + *
  1. Initializes the Flutter system.
  2. + *
  3. Obtains or creates a {@link FlutterEngine}.
  4. + *
  5. Creates and configures a {@link PlatformPlugin}.
  6. + *
  7. Attaches the {@link FlutterEngine} to the surrounding {@code Activity}, if desired.
  8. + *
  9. Configures the {@link FlutterEngine} via + * {@link Host#configureFlutterEngine(FlutterEngine)}.
  10. + *
+ */ + 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) { + 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. + // + // Passing this Fragment's Lifecycle should be sufficient because as long as this Fragment + // is attached to its Activity, the lifecycles should be in sync. Once this Fragment is + // detached from its Activity, that Activity will be detached from the FlutterEngine, too, + // 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.d(TAG, "Attaching FlutterEngine to the Activity that owns this Fragment."); + flutterEngine.getActivityControlSurface().attachToActivity( + host.getActivity(), + host.getLifecycle() + ); + } + + 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}. + *

+ * First, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via + * {@link Host#provideFlutterEngine(Context)}. + *

+ * If the {@code host} does not provide a {@link FlutterEngine}, then a new {@link FlutterEngine} + * is instantiated. + */ + private void setupFlutterEngine() { + Log.d(TAG, "Setting up FlutterEngine."); + + // First, defer to subclasses for a custom FlutterEngine. + flutterEngine = host.provideFlutterEngine(host.getContext()); + if (flutterEngine != null) { + return; + } + + // Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our + // FlutterView. + Log.d(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for" + + " this FlutterFragment."); + flutterEngine = new FlutterEngine(host.getContext()); + isFlutterEngineFromHost = false; + } + + /** + * Invoke this method from {@code Activity#onCreate(Bundle)} to create the content {@code View}, + * or from {@code Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. + *

+ * {@code inflater} and {@code container} may be null when invoked from an {@code Activity}. + *

+ * This method creates a new {@link FlutterView}, adds a {@link OnFirstFrameRenderedListener} to + * it, and then returns it. + */ + @NonNull + View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + Log.v(TAG, "Creating FlutterView."); + ensureAlive(); + flutterView = new FlutterView(host.getActivity(), host.getRenderMode(), host.getTransparencyMode()); + flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); + + flutterSplashView = new FlutterSplashView(host.getContext()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + flutterSplashView.setId(View.generateViewId()); + } else { + // TODO(mattcarroll): Find a better solution to this ID. This is a random, static ID. + // It might conflict with other Views, and it means that only a single FlutterSplashView + // can exist in a View hierarchy at one time. + flutterSplashView.setId(486947586); + } + flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen()); + + return flutterSplashView; + } + + /** + * Invoke this from {@code Activity#onStart()} or {@code Fragment#onStart()}. + *

+ * This method: + *

+ *

    + *
  1. Attaches the {@link FlutterEngine} owned by this delegate to the {@link FlutterView} + * owned by this delegate.
  2. + *
  3. Begins executing Dart code, if it is not already executing.
  4. + *
+ */ + void onStart() { + Log.v(TAG, "onStart()"); + ensureAlive(); + + // We post() the code that attaches the FlutterEngine to our FlutterView because there is + // some kind of blocking logic on the native side when the surface is connected. That lag + // causes launching Activitys to wait a second or two before launching. By post()'ing this + // behavior we are able to move this blocking logic to after the Activity's launch. + // TODO(mattcarroll): figure out how to avoid blocking the MAIN thread when connecting a surface + new Handler().post(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Attaching FlutterEngine to FlutterView."); + flutterView.attachToFlutterEngine(flutterEngine); + + doInitialFlutterViewRun(); + } + }); + } + + /** + * Starts running Dart within the FlutterView for the first time. + *

+ * Reloading/restarting Dart within a given FlutterView is not supported. If this method is + * invoked while Dart is already executing then it does nothing. + *

+ * {@code flutterEngine} must be non-null when invoking this method. + */ + private void doInitialFlutterViewRun() { + if (flutterEngine.getDartExecutor().isExecutingDart()) { + // No warning is logged because this situation will happen on every config + // change if the developer does not choose to retain the Fragment instance. + // So this is expected behavior in many cases. + return; + } + + Log.d(TAG, "Executing Dart entrypoint: " + host.getDartEntrypointFunctionName() + + ", and sending initial route: " + host.getInitialRoute()); + + // The engine needs to receive the Flutter app's initial route before executing any + // Dart code to ensure that the initial route arrives in time to be applied. + if (host.getInitialRoute() != null) { + flutterEngine.getNavigationChannel().setInitialRoute(host.getInitialRoute()); + } + + // Configure the Dart entrypoint and execute it. + DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint( + host.getContext().getResources().getAssets(), + host.getAppBundlePath(), + host.getDartEntrypointFunctionName() + ); + flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); + } + + /** + * Invoke this from {@code Activity#onResume()} or {@code Fragment#onResume()}. + *

+ * This method notifies the running Flutter app that it is "resumed" as per the Flutter app + * lifecycle. + */ + void onResume() { + Log.v(TAG, "onResume()"); + ensureAlive(); + flutterEngine.getLifecycleChannel().appIsResumed(); + } + + /** + * Invoke this from {@code Activity#onPostResume()}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method informs the {@link PlatformPlugin} that {@code onPostResume()} has run, which + * is used to update system UI overlays. + */ + // TODO(mattcarroll): determine why this can't be in onResume(). Comment reason, or move if possible. + void onPostResume() { + Log.v(TAG, "onPostResume()"); + ensureAlive(); + if (flutterEngine != null) { + if (platformPlugin != null) { + // TODO(mattcarroll): find a better way to handle the update of UI overlays than calling through + // to platformPlugin. We're implicitly entangling the Window, Activity, Fragment, + // and engine all with this one call. + platformPlugin.updateSystemUiOverlays(); + } + } else { + Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@code Activity#onPause()} or {@code Fragment#onPause()}. + *

+ * This method notifies the running Flutter app that it is "inactive" as per the Flutter app + * lifecycle. + */ + void onPause() { + Log.v(TAG, "onPause()"); + ensureAlive(); + flutterEngine.getLifecycleChannel().appIsInactive(); + } + + /** + * Invoke this from {@code Activity#onStop()} or {@code Fragment#onStop()}. + *

+ * This method: + *

+ *

    + *
  1. This method notifies the running Flutter app that it is "paused" as per the Flutter app + * lifecycle.
  2. + *
  3. Detaches this delegate's {@link FlutterEngine} from this delegate's {@link FlutterView}.
  4. + *
+ */ + void onStop() { + Log.v(TAG, "onStop()"); + ensureAlive(); + flutterEngine.getLifecycleChannel().appIsPaused(); + flutterView.detachFromFlutterEngine(); + } + + /** + * Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDestroyView()}. + *

+ * This method removes this delegate's {@link FlutterView}'s {@link OnFirstFrameRenderedListener}. + */ + void onDestroyView() { + Log.v(TAG, "onDestroyView()"); + ensureAlive(); + flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener); + } + + /** + * Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDetach()}. + *

+ * This method: + *

+ *

    + *
  1. Detaches this delegate's {@link FlutterEngine} from its surrounding {@code Activity}, + * if it was previously attached.
  2. + *
  3. Destroys this delegate's {@link PlatformPlugin}.
  4. + *
  5. Destroys this delegate's {@link FlutterEngine} if + * {@link Host#retainFlutterEngineAfterHostDestruction()} returns false.
  6. + *
+ */ + void onDetach() { + Log.v(TAG, "onDetach()"); + ensureAlive(); + + if (host.shouldAttachEngineToActivity()) { + // Notify plugins that they are no longer attached to an Activity. + Log.d(TAG, "Detaching FlutterEngine from the Activity that owns this Fragment."); + if (host.getActivity().isChangingConfigurations()) { + flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges(); + } else { + flutterEngine.getActivityControlSurface().detachFromActivity(); + } + } + + // Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment, + // and this Fragment's Activity. + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + + // Destroy our FlutterEngine if we're not set to retain it. + if (!host.retainFlutterEngineAfterHostDestruction() && !isFlutterEngineFromHost) { + flutterEngine.destroy(); + flutterEngine = null; + } + } + + /** + * Invoke this from {@link Activity#onBackPressed()}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method instructs Flutter's navigation system to "pop route". + */ + void onBackPressed() { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding onBackPressed() to FlutterEngine."); + flutterEngine.getNavigationChannel().popRoute(); + } else { + Log.w(TAG, "Invoked onBackPressed() before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link Activity#onRequestPermissionsResult(int, String[], int[])} or + * {@code Fragment#onRequestPermissionsResult(int, String[], int[])}. + *

+ * This method forwards to interested Flutter plugins. + */ + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding onRequestPermissionsResult() to FlutterEngine:\n" + + "requestCode: " + requestCode + "\n" + + "permissions: " + Arrays.toString(permissions) + "\n" + + "grantResults: " + Arrays.toString(grantResults)); + flutterEngine.getActivityControlSurface().onRequestPermissionsResult(requestCode, permissions, grantResults); + } else { + Log.w(TAG, "onRequestPermissionResult() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@code Activity#onNewIntent(Intent)}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method forwards to interested Flutter plugins. + */ + void onNewIntent(@NonNull Intent intent) { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding onNewIntent() to FlutterEngine."); + flutterEngine.getActivityControlSurface().onNewIntent(intent); + } else { + Log.w(TAG, "onNewIntent() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@code Activity#onActivityResult(int, int, Intent)} or + * {@code Fragment#onActivityResult(int, int, Intent)}. + *

+ * This method forwards to interested Flutter plugins. + */ + void onActivityResult(int requestCode, int resultCode, Intent data) { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding onActivityResult() to FlutterEngine:\n" + + "requestCode: " + requestCode + "\n" + + "resultCode: " + resultCode + "\n" + + "data: " + data); + flutterEngine.getActivityControlSurface().onActivityResult(requestCode, resultCode, data); + } else { + Log.w(TAG, "onActivityResult() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@code Activity#onUserLeaveHint()}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method forwards to interested Flutter plugins. + */ + void onUserLeaveHint() { + ensureAlive(); + if (flutterEngine != null) { + Log.v(TAG, "Forwarding onUserLeaveHint() to FlutterEngine."); + flutterEngine.getActivityControlSurface().onUserLeaveHint(); + } else { + Log.w(TAG, "onUserLeaveHint() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link Activity#onTrimMemory(int)}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method sends a "memory pressure warning" message to Flutter over the "system channel". + */ + void onTrimMemory(int level) { + ensureAlive(); + if (flutterEngine != null) { + // Use a trim level delivered while the application is running so the + // framework has a chance to react to the notification. + if (level == TRIM_MEMORY_RUNNING_LOW) { + Log.v(TAG, "Forwarding onTrimMemory() to FlutterEngine. Level: " + level); + flutterEngine.getSystemChannel().sendMemoryPressureWarning(); + } + } else { + Log.w(TAG, "onTrimMemory() invoked before FlutterFragment was attached to an Activity."); + } + } + + /** + * Invoke this from {@link Activity#onLowMemory()}. + *

+ * A {@code Fragment} host must have its containing {@code Activity} forward this call so that + * the {@code Fragment} can then invoke this method. + *

+ * This method sends a "memory pressure warning" message to Flutter over the "system channel". + */ + void onLowMemory() { + Log.v(TAG, "Forwarding onLowMemory() to FlutterEngine."); + ensureAlive(); + flutterEngine.getSystemChannel().sendMemoryPressureWarning(); + } + + /** + * Ensures that this delegate has not been {@link #release()}'ed. + *

+ * An {@code IllegalStateException} is thrown if this delegate has been {@link #release()}'ed. + */ + private void ensureAlive() { + if (host == null) { + throw new IllegalStateException("Cannot execute method on a destroyed FlutterActivityAndFragmentDelegate."); + } + } + + /** + * The {@link FlutterActivity} or {@link FlutterFragment} that owns this + * {@code FlutterActivityAndFragmentDelegate}. + */ + /* package */ interface Host extends SplashScreenProvider, FlutterEngineProvider, FlutterEngineConfigurator { + /** + * Returns the {@link Context} that backs the host {@link Activity} or {@code Fragment}. + */ + @NonNull + Context getContext(); + + /** + * Returns the host {@link Activity} or the {@code Activity} that is currently attached + * to the host {@code Fragment}. + */ + @Nullable + Activity getActivity(); + + /** + * Returns the {@link Lifecycle} that backs the host {@link Activity} or {@code Fragment}. + */ + @NonNull + Lifecycle getLifecycle(); + + /** + * Returns the {@link FlutterShellArgs} that should be used when initializing Flutter. + */ + @NonNull + FlutterShellArgs getFlutterShellArgs(); + + /** + * Returns the Dart entrypoint that should run when a new {@link FlutterEngine} is + * created. + */ + @NonNull + String getDartEntrypointFunctionName(); + + /** + * Returns the path to the app bundle where the Dart code exists. + */ + @NonNull + String getAppBundlePath(); + + /** + * Returns the initial route that Flutter renders. + */ + @Nullable + String getInitialRoute(); + + /** + * Returns the {@link FlutterView.RenderMode} used by the {@link FlutterView} that + * displays the {@link FlutterEngine}'s content. + */ + @NonNull + FlutterView.RenderMode getRenderMode(); + + /** + * Returns the {@link FlutterView.TransparencyMode} used by the {@link FlutterView} that + * displays the {@link FlutterEngine}'s content. + */ + @NonNull + FlutterView.TransparencyMode getTransparencyMode(); + + @Nullable + SplashScreen provideSplashScreen(); + + /** + * Returns the {@link FlutterEngine} that should be rendered to a {@link FlutterView}. + *

+ * If {@code null} is returned, a new {@link FlutterEngine} will be created automatically. + */ + @Nullable + FlutterEngine provideFlutterEngine(@NonNull Context context); + + /** + * Hook for the host to create/provide a {@link PlatformPlugin} if the associated + * Flutter experience should control system chrome. + */ + @Nullable + PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine); + + /** + * Hook for the host to configure the {@link FlutterEngine} as desired. + */ + void configureFlutterEngine(@NonNull FlutterEngine flutterEngine); + + /** + * Returns true if the {@link FlutterEngine}'s plugin system should be connected to the + * host {@link Activity}, allowing plugins to interact with it. + */ + boolean shouldAttachEngineToActivity(); + + /** + * Returns true if the {@link FlutterEngine} used in this delegate should outlive the + * delegate. + *

+ * If {@code false} is returned, the {@link FlutterEngine} used in this delegate will be + * destroyed when the delegate is destroyed. + */ + boolean retainFlutterEngineAfterHostDestruction(); + + /** + * Invoked by this delegate when its {@link FlutterView} has rendered its first Flutter + * frame. + */ + void onFirstFrameRendered(); + } +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java new file mode 100644 index 00000000000..f11c4f243e5 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java @@ -0,0 +1,33 @@ +// 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 android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.support.annotation.NonNull; +import android.support.v4.app.FragmentActivity; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Configures a {@link FlutterEngine} after it is created, e.g., adds plugins. + *

+ * This interface may be applied to a {@link FragmentActivity} that owns a {@code FlutterFragment}. + */ +public interface FlutterEngineConfigurator { + /** + * Configures the given {@link FlutterEngine}. + *

+ * 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)}. + *

+ * 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 configuration, e.g., plugins, must not expect or depend upon an available + * {@code Activity} at the time that this method is invoked. + */ + void configureFlutterEngine(@NonNull FlutterEngine flutterEngine); +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java new file mode 100644 index 00000000000..4fb9c0bd731 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java @@ -0,0 +1,33 @@ +// 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 android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Provides a {@link FlutterEngine} instance to be used by a {@code FlutterActivity} or + * {@code FlutterFragment}. + *

+ * {@link FlutterEngine} instances require significant time to warm up. Therefore, a developer + * might choose to hold onto an existing {@link FlutterEngine} and connect it to various + * {@link FlutterActivity}s and/or {@code FlutterFragment}s. This interface facilitates providing + * a cached, pre-warmed {@link FlutterEngine}. + */ +public interface FlutterEngineProvider { + /** + * Returns the {@link FlutterEngine} that should be used by a child {@code FlutterFragment}. + *

+ * This method may return a new {@link FlutterEngine}, an existing, cached {@link FlutterEngine}, + * or null to express that the {@code FlutterEngineProvider} would like the {@code FlutterFragment} + * to provide its own {@code FlutterEngine} instance. + */ + @Nullable + FlutterEngine provideFlutterEngine(@NonNull Context context); +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index b14b845036a..c004d7c8c4a 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -4,15 +4,12 @@ package io.flutter.embedding.android; -import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; - import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -21,12 +18,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import java.util.Arrays; - import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterShellArgs; -import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.view.FlutterMain; @@ -34,18 +28,15 @@ import io.flutter.view.FlutterMain; /** * {@code Fragment} which displays a Flutter UI that takes up all available {@code Fragment} space. *

- * WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE. - * IF YOU USE IT, WE WILL BREAK YOU. - *

* Using a {@code FlutterFragment} requires forwarding a number of calls from an {@code Activity} to * ensure that the internal Flutter app behaves as expected: *

    - *
  1. {@link android.app.Activity#onPostResume()}
  2. - *
  3. {@link android.app.Activity#onBackPressed()}
  4. - *
  5. {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])} ()}
  6. - *
  7. {@link android.app.Activity#onNewIntent(Intent)} ()}
  8. - *
  9. {@link android.app.Activity#onUserLeaveHint()}
  10. - *
  11. {@link android.app.Activity#onTrimMemory(int)}
  12. + *
  13. {@link #onPostResume()}
  14. + *
  15. {@link #onBackPressed()}
  16. + *
  17. {@link #onRequestPermissionsResult(int, String[], int[])} ()}
  18. + *
  19. {@link #onNewIntent(Intent)} ()}
  20. + *
  21. {@link #onUserLeaveHint()}
  22. + *
  23. {@link #onTrimMemory(int)}
  24. *
* Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be sure * to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than @@ -61,26 +52,51 @@ import io.flutter.view.FlutterMain; * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a * {@code Fragment}. */ -public class FlutterFragment extends Fragment { +public class FlutterFragment extends Fragment implements FlutterActivityAndFragmentDelegate.Host { private static final String TAG = "FlutterFragment"; + /** + * The Dart entrypoint method name that is executed upon initialization. + */ protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint"; + /** + * Initial Flutter route that is rendered in a Navigator widget. + */ protected static final String ARG_INITIAL_ROUTE = "initial_route"; + /** + * Path to Flutter's Dart code. + */ protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path"; + /** + * Flutter shell arguments. + */ protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args"; + /** + * {@link FlutterView.RenderMode} to be used for the {@link FlutterView} in this + * {@code FlutterFragment} + */ protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode"; + /** + * {@link FlutterView.TransparencyMode} to be used for the {@link FlutterView} in this + * {@code FlutterFragment} + */ protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode"; + /** + * See {@link #shouldAttachEngineToActivity()}. + */ protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity"; + @NonNull + public static FlutterFragment createDefaultFlutterFragment() { + return new FlutterFragment.Builder().build(); + } + /** * Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond * to the values set on this {@code Builder}. *

- * To create a {@code FlutterFragment} with default {@code arguments}, invoke {@code build()} - * without setting any builder properties: - * {@code - * FlutterFragment fragment = new FlutterFragment.Builder().build(); - * } + * To create a {@code FlutterFragment} with default {@code arguments}, invoke + * {@link #createDefaultFlutterFragment()}. *

* Subclasses of {@code FlutterFragment} that do not introduce any new arguments can use this * {@code Builder} to construct instances of the subclass without subclassing this {@code Builder}. @@ -291,15 +307,10 @@ public class FlutterFragment extends Fragment { } } - @Nullable - private FlutterEngine flutterEngine; - private boolean isFlutterEngineFromActivity; - @Nullable - private FlutterSplashView flutterSplashView; - @Nullable - private FlutterView flutterView; - @Nullable - private PlatformPlugin platformPlugin; + // Delegate that runs all lifecycle and OS hook logic that is common between + // FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate + // implementation for details about why it exists. + private FlutterActivityAndFragmentDelegate delegate; private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { @Override @@ -309,7 +320,7 @@ public class FlutterFragment extends Fragment { // Notify our owning Activity that the first frame has been rendered. FragmentActivity fragmentActivity = getActivity(); - if (fragmentActivity != null && fragmentActivity instanceof OnFirstFrameRenderedListener) { + if (fragmentActivity instanceof OnFirstFrameRenderedListener) { OnFirstFrameRenderedListener activityAsListener = (OnFirstFrameRenderedListener) fragmentActivity; activityAsListener.onFirstFrameRendered(); } @@ -322,496 +333,144 @@ public class FlutterFragment extends Fragment { setArguments(new Bundle()); } - /** - * The {@link FlutterEngine} that backs the Flutter content presented by this {@code Fragment}. - * - * @return the {@link FlutterEngine} held by this {@code Fragment} - */ - @Nullable - public FlutterEngine getFlutterEngine() { - return flutterEngine; - } - @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - - initializeFlutter(getContextCompat()); - - // 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) { - 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 = new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); - - if (shouldAttachEngineToActivity()) { - // Notify any plugins that are currently attached to our FlutterEngine that they - // are now attached to an Activity. - // - // Passing this Fragment's Lifecycle should be sufficient because as long as this Fragment - // is attached to its Activity, the lifecycles should be in sync. Once this Fragment is - // detached from its Activity, that Activity will be detached from the FlutterEngine, too, - // 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.d(TAG, "Attaching FlutterEngine to the Activity that owns this Fragment."); - flutterEngine.getActivityControlSurface().attachToActivity( - getActivity(), - getLifecycle() - ); - } - - configureFlutterEngine(flutterEngine); - } - - private void initializeFlutter(@NonNull Context context) { - String[] flutterShellArgsArray = getArguments().getStringArray(ARG_FLUTTER_INITIALIZATION_ARGS); - FlutterShellArgs flutterShellArgs = new FlutterShellArgs( - flutterShellArgsArray != null ? flutterShellArgsArray : new String[] {} - ); - - FlutterMain.ensureInitializationComplete(context.getApplicationContext(), flutterShellArgs.toArray()); - } - - /** - * Obtains a reference to a FlutterEngine to back this {@code FlutterFragment}. - *

- * First, {@code FlutterFragment} subclasses are given an opportunity to provide a - * {@link FlutterEngine} by overriding {@link #createFlutterEngine(Context)}. - *

- * Second, the {@link FragmentActivity} that owns this {@code FlutterFragment} is - * given the opportunity to provide a {@link FlutterEngine} as a {@link FlutterEngineProvider}. - *

- * If subclasses do not provide a {@link FlutterEngine}, and the owning {@link FragmentActivity} - * does not implement {@link FlutterEngineProvider} or chooses to return {@code null}, then a new - * {@link FlutterEngine} is instantiated. - */ - private void setupFlutterEngine() { - Log.d(TAG, "Setting up FlutterEngine."); - - // First, defer to subclasses for a custom FlutterEngine. - flutterEngine = createFlutterEngine(getContextCompat()); - if (flutterEngine != null) { - return; - } - - // Second, defer to the FragmentActivity that owns us to see if it wants to provide a - // FlutterEngine. - FragmentActivity attachedActivity = getActivity(); - if (attachedActivity instanceof FlutterEngineProvider) { - // Defer to the Activity that owns us to provide a FlutterEngine. - Log.d(TAG, "Deferring to attached Activity to provide a FlutterEngine."); - FlutterEngineProvider flutterEngineProvider = (FlutterEngineProvider) attachedActivity; - flutterEngine = flutterEngineProvider.provideFlutterEngine(getContext()); - if (flutterEngine != null) { - isFlutterEngineFromActivity = true; - return; - } - } - - // Neither our subclass, nor our owning Activity wanted to provide a custom FlutterEngine. - // Create a FlutterEngine to back our FlutterView. - Log.d(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for" - + " this FlutterFragment."); - flutterEngine = new FlutterEngine(getContext()); - isFlutterEngineFromActivity = false; - } - - /** - * Hook for subclasses to return a {@link FlutterEngine} with whatever configuration - * is desired. - *

- * This method takes precedence for creation of a {@link FlutterEngine} over any owning - * {@code Activity} that may implement {@link FlutterEngineProvider}. - *

- * Consider returning a cached {@link FlutterEngine} instance from this method to avoid the - * typical warm-up time that a new {@link FlutterEngine} instance requires. - *

- * If null is returned then a new default {@link FlutterEngine} will be created to back this - * {@code FlutterFragment}. - */ - @Nullable - protected FlutterEngine createFlutterEngine(@NonNull Context context) { - return null; - } - - /** - * Configures a {@link FlutterEngine} after its creation. - *

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

- * 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 configuration, e.g., plugins, must not expect or depend upon an available - * {@code Activity} at the time that this method is invoked. - *

- * The default behavior of this method is to defer to the owning {@code FragmentActivity} - * as a {@link FlutterEngineConfigurator}. Subclasses can override this method if the - * subclass needs to override the {@code FragmentActivity}'s behavior, or add to it. - */ - protected void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - FragmentActivity attachedActivity = getActivity(); - if (attachedActivity instanceof FlutterEngineConfigurator) { - ((FlutterEngineConfigurator) attachedActivity).configureFlutterEngine(flutterEngine); - } + delegate = new FlutterActivityAndFragmentDelegate(this); + delegate.onAttach(context); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Log.v(TAG, "Creating FlutterView."); - flutterView = new FlutterView(getActivity(), getRenderMode(), getTransparencyMode()); - flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); - - flutterSplashView = new FlutterSplashView(getContext()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - flutterSplashView.setId(View.generateViewId()); - } else { - // TODO(mattcarroll): Find a better solution to this ID. This is a random, static ID. - // It might conflict with other Views, and it means that only a single FlutterSplashView - // can exist in a View hierarchy at one time. - flutterSplashView.setId(486947586); - } - flutterSplashView.displayFlutterViewWithSplash(flutterView, provideSplashScreen()); - - return flutterSplashView; - } - - @Nullable - protected SplashScreen provideSplashScreen() { - FragmentActivity parentActivity = getActivity(); - if (parentActivity instanceof SplashScreenProvider) { - SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity; - return splashScreenProvider.provideSplashScreen(); - } - - return null; - } - - /** - * Starts running Dart within the FlutterView for the first time. - * - * Reloading/restarting Dart within a given FlutterView is not supported. If this method is - * invoked while Dart is already executing then it does nothing. - * - * {@code flutterEngine} must be non-null when invoking this method. - */ - private void doInitialFlutterViewRun() { - if (flutterEngine.getDartExecutor().isExecutingDart()) { - // No warning is logged because this situation will happen on every config - // change if the developer does not choose to retain the Fragment instance. - // So this is expected behavior in many cases. - return; - } - - Log.d(TAG, "Executing Dart entrypoint: " + getDartEntrypointFunctionName() - + ", and sending initial route: " + getInitialRoute()); - - // The engine needs to receive the Flutter app's initial route before executing any - // Dart code to ensure that the initial route arrives in time to be applied. - if (getInitialRoute() != null) { - flutterEngine.getNavigationChannel().setInitialRoute(getInitialRoute()); - } - - // Configure the Dart entrypoint and execute it. - DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint( - getResources().getAssets(), - getAppBundlePath(), - getDartEntrypointFunctionName() - ); - flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); - } - - /** - * Returns the initial route that should be rendered within Flutter, once the Flutter app starts. - * - * Defaults to {@code null}, which signifies a route of "/" in Flutter. - */ - @Nullable - protected String getInitialRoute() { - return getArguments().getString(ARG_INITIAL_ROUTE); - } - - /** - * Returns the file path to the desired Flutter app's bundle of code. - * - * Defaults to {@link FlutterMain#findAppBundlePath(Context)}. - */ - @NonNull - protected String getAppBundlePath() { - return getArguments().getString(ARG_APP_BUNDLE_PATH, FlutterMain.findAppBundlePath(getContextCompat())); - } - - /** - * Returns the name of the Dart method that this {@code FlutterFragment} should execute to - * start a Flutter app. - * - * Defaults to "main". - */ - @NonNull - protected String getDartEntrypointFunctionName() { - return getArguments().getString(ARG_DART_ENTRYPOINT, "main"); - } - - /** - * Returns the desired {@link FlutterView.RenderMode} for the {@link FlutterView} displayed in - * this {@code FlutterFragment}. - * - * Defaults to {@link FlutterView.RenderMode#surface}. - */ - @NonNull - protected FlutterView.RenderMode getRenderMode() { - String renderModeName = getArguments().getString(ARG_FLUTTERVIEW_RENDER_MODE, FlutterView.RenderMode.surface.name()); - return FlutterView.RenderMode.valueOf(renderModeName); - } - - /** - * Returns the desired {@link FlutterView.TransparencyMode} for the {@link FlutterView} displayed in - * this {@code FlutterFragment}. - *

- * Defaults to {@link FlutterView.TransparencyMode#transparent}. - */ - @NonNull - protected FlutterView.TransparencyMode getTransparencyMode() { - String transparencyModeName = getArguments().getString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, FlutterView.TransparencyMode.transparent.name()); - return FlutterView.TransparencyMode.valueOf(transparencyModeName); + return delegate.onCreateView(inflater, container, savedInstanceState); } @Override public void onStart() { super.onStart(); - Log.v(TAG, "onStart()"); - - // We post() the code that attaches the FlutterEngine to our FlutterView because there is - // some kind of blocking logic on the native side when the surface is connected. That lag - // causes launching Activitys to wait a second or two before launching. By post()'ing this - // behavior we are able to move this blocking logic to after the Activity's launch. - // TODO(mattcarroll): figure out how to avoid blocking the MAIN thread when connecting a surface - new Handler().post(new Runnable() { - @Override - public void run() { - Log.v(TAG, "Attaching FlutterEngine to FlutterView."); - flutterView.attachToFlutterEngine(flutterEngine); - - doInitialFlutterViewRun(); - } - }); + delegate.onStart(); } @Override public void onResume() { super.onResume(); - Log.v(TAG, "onResume()"); - flutterEngine.getLifecycleChannel().appIsResumed(); + delegate.onResume(); } // TODO(mattcarroll): determine why this can't be in onResume(). Comment reason, or move if possible. + @ActivityCallThrough public void onPostResume() { - Log.v(TAG, "onPostResume()"); - if (flutterEngine != null) { - // TODO(mattcarroll): find a better way to handle the update of UI overlays than calling through - // to platformPlugin. We're implicitly entangling the Window, Activity, Fragment, - // and engine all with this one call. - platformPlugin.onPostResume(); - } else { - Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onPostResume(); } @Override public void onPause() { super.onPause(); - Log.v(TAG, "onPause()"); - flutterEngine.getLifecycleChannel().appIsInactive(); + delegate.onPause(); } @Override public void onStop() { super.onStop(); - Log.v(TAG, "onStop()"); - flutterEngine.getLifecycleChannel().appIsPaused(); - flutterView.detachFromFlutterEngine(); + delegate.onStop(); } @Override public void onDestroyView() { super.onDestroyView(); - Log.v(TAG, "onDestroyView()"); - flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener); + delegate.onDestroyView(); } @Override public void onDetach() { super.onDetach(); - Log.v(TAG, "onDetach()"); - - if (shouldAttachEngineToActivity()) { - // Notify plugins that they are no longer attached to an Activity. - Log.d(TAG, "Detaching FlutterEngine from the Activity that owns this Fragment."); - if (getActivity().isChangingConfigurations()) { - flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges(); - } else { - flutterEngine.getActivityControlSurface().detachFromActivity(); - } - } - - // Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment, - // and this Fragment's Activity. - platformPlugin.destroy(); - platformPlugin = null; - - // Destroy our FlutterEngine if we're not set to retain it. - if (!retainFlutterEngineAfterFragmentDestruction() && !isFlutterEngineFromActivity) { - flutterEngine.destroy(); - flutterEngine = null; - } - } - - /** - * Returns true if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive - * the {@code FlutterFragment}, itself. - * - * Defaults to false. This method can be overridden in subclasses to retain the - * {@link FlutterEngine}. - */ - // TODO(mattcarroll): consider a dynamic determination of this preference based on whether the - // engine was created automatically, or if the engine was provided manually. - // Manually provided engines should probably not be destroyed. - protected boolean retainFlutterEngineAfterFragmentDestruction() { - return false; - } - - protected boolean shouldAttachEngineToActivity() { - return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY); - } - - /** - * The hardware back button was pressed. - * - * See {@link android.app.Activity#onBackPressed()} - */ - public void onBackPressed() { - if (flutterEngine != null) { - Log.v(TAG, "Forwarding onBackPressed() to FlutterEngine."); - flutterEngine.getNavigationChannel().popRoute(); - } else { - Log.w(TAG, "Invoked onBackPressed() before FlutterFragment was attached to an Activity."); - } + delegate.onDetach(); + delegate.release(); + delegate = null; } /** * The result of a permission request has been received. - * + *

* See {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])} - * + *

* @param requestCode identifier passed with the initial permission request * @param permissions permissions that were requested * @param grantResults permission grants or denials */ + @ActivityCallThrough public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (flutterEngine != null) { - Log.v(TAG, "Forwarding onRequestPermissionsResult() to FlutterEngine:\n" - + "requestCode: " + requestCode + "\n" - + "permissions: " + Arrays.toString(permissions) + "\n" - + "grantResults: " + Arrays.toString(grantResults)); - flutterEngine.getActivityControlSurface().onRequestPermissionsResult(requestCode, permissions, grantResults); - } else { - Log.w(TAG, "onRequestPermissionResult() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onRequestPermissionsResult(requestCode, permissions, grantResults); } /** * A new Intent was received by the {@link android.app.Activity} that currently owns this * {@link Fragment}. - * + *

* See {@link android.app.Activity#onNewIntent(Intent)} - * + *

* @param intent new Intent */ + @ActivityCallThrough public void onNewIntent(@NonNull Intent intent) { - if (flutterEngine != null) { - Log.v(TAG, "Forwarding onNewIntent() to FlutterEngine."); - flutterEngine.getActivityControlSurface().onNewIntent(intent); - } else { - Log.w(TAG, "onNewIntent() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onNewIntent(intent); + } + + /** + * The hardware back button was pressed. + *

+ * See {@link android.app.Activity#onBackPressed()} + */ + @ActivityCallThrough + public void onBackPressed() { + delegate.onBackPressed(); } /** * A result has been returned after an invocation of {@link Fragment#startActivityForResult(Intent, int)}. - * + *

* @param requestCode request code sent with {@link Fragment#startActivityForResult(Intent, int)} * @param resultCode code representing the result of the {@code Activity} that was launched * @param data any corresponding return data, held within an {@code Intent} */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (flutterEngine != null) { - Log.v(TAG, "Forwarding onActivityResult() to FlutterEngine:\n" - + "requestCode: " + requestCode + "\n" - + "resultCode: " + resultCode + "\n" - + "data: " + data); - flutterEngine.getActivityControlSurface().onActivityResult(requestCode, resultCode, data); - } else { - Log.w(TAG, "onActivityResult() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onActivityResult(requestCode, resultCode, data); } /** * The {@link android.app.Activity} that owns this {@link Fragment} is about to go to the background * as the result of a user's choice/action, i.e., not as the result of an OS decision. - * + *

* See {@link android.app.Activity#onUserLeaveHint()} */ + @ActivityCallThrough public void onUserLeaveHint() { - if (flutterEngine != null) { - Log.v(TAG, "Forwarding onUserLeaveHint() to FlutterEngine."); - flutterEngine.getActivityControlSurface().onUserLeaveHint(); - } else { - Log.w(TAG, "onUserLeaveHint() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onUserLeaveHint(); } /** * Callback invoked when memory is low. - * + *

* This implementation forwards a memory pressure warning to the running Flutter app. - * + *

* @param level level */ + @ActivityCallThrough public void onTrimMemory(int level) { - if (flutterEngine != null) { - // Use a trim level delivered while the application is running so the - // framework has a chance to react to the notification. - if (level == TRIM_MEMORY_RUNNING_LOW) { - Log.v(TAG, "Forwarding onTrimMemory() to FlutterEngine. Level: " + level); - flutterEngine.getSystemChannel().sendMemoryPressureWarning(); - } - } else { - Log.w(TAG, "onTrimMemory() invoked before FlutterFragment was attached to an Activity."); - } + delegate.onTrimMemory(level); } /** * Callback invoked when memory is low. - * + *

* This implementation forwards a memory pressure warning to the running Flutter app. */ @Override public void onLowMemory() { super.onLowMemory(); - Log.v(TAG, "Forwarding onLowMemory() to FlutterEngine."); - flutterEngine.getSystemChannel().sendMemoryPressureWarning(); + delegate.onLowMemory(); } @NonNull @@ -821,74 +480,237 @@ public class FlutterFragment extends Fragment { : getActivity(); } + /** + * {@link FlutterActivityAndFragmentDelegate.Host} method that is used by + * {@link FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when + * initializing Flutter. + */ + @Override + @NonNull + public FlutterShellArgs getFlutterShellArgs() { + String[] flutterShellArgsArray = getArguments().getStringArray(ARG_FLUTTER_INITIALIZATION_ARGS); + return new FlutterShellArgs( + flutterShellArgsArray != null ? flutterShellArgsArray : new String[] {} + ); + } + + /** + * Returns the name of the Dart method that this {@code FlutterFragment} should execute to + * start a Flutter app. + *

+ * Defaults to "main". + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public String getDartEntrypointFunctionName() { + return getArguments().getString(ARG_DART_ENTRYPOINT, "main"); + } + + /** + * Returns the file path to the desired Flutter app's bundle of code. + *

+ * Defaults to {@link FlutterMain#findAppBundlePath(Context)}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public String getAppBundlePath() { + return getArguments().getString(ARG_APP_BUNDLE_PATH, FlutterMain.findAppBundlePath(getContextCompat())); + } + + /** + * Returns the initial route that should be rendered within Flutter, once the Flutter app starts. + *

+ * Defaults to {@code null}, which signifies a route of "/" in Flutter. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @Nullable + public String getInitialRoute() { + return getArguments().getString(ARG_INITIAL_ROUTE); + } + + /** + * Returns the desired {@link FlutterView.RenderMode} for the {@link FlutterView} displayed in + * this {@code FlutterFragment}. + *

+ * Defaults to {@link FlutterView.RenderMode#surface}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public FlutterView.RenderMode getRenderMode() { + String renderModeName = getArguments().getString( + ARG_FLUTTERVIEW_RENDER_MODE, + FlutterView.RenderMode.surface.name() + ); + return FlutterView.RenderMode.valueOf(renderModeName); + } + + /** + * Returns the desired {@link FlutterView.TransparencyMode} for the {@link FlutterView} displayed in + * this {@code FlutterFragment}. + *

+ * Defaults to {@link FlutterView.TransparencyMode#transparent}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public FlutterView.TransparencyMode getTransparencyMode() { + String transparencyModeName = getArguments().getString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + FlutterView.TransparencyMode.transparent.name() + ); + return FlutterView.TransparencyMode.valueOf(transparencyModeName); + } + + @Override + @Nullable + public SplashScreen provideSplashScreen() { + FragmentActivity parentActivity = getActivity(); + if (parentActivity instanceof SplashScreenProvider) { + SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity; + return splashScreenProvider.provideSplashScreen(); + } + + return null; + } + + /** + * Hook for subclasses to return a {@link FlutterEngine} with whatever configuration + * is desired. + *

+ * By default this method defers to this {@code FlutterFragment}'s surrounding {@code Activity}, + * if that {@code Activity} implements {@link FlutterEngineProvider}. If this method is + * overridden, the surrounding {@code Activity} will no longer be given an opportunity to + * provide a {@link FlutterEngine}, unless the subclass explicitly implements that behavior. + *

+ * Consider returning a cached {@link FlutterEngine} instance from this method to avoid the + * typical warm-up time that a new {@link FlutterEngine} instance requires. + *

+ * If null is returned then a new default {@link FlutterEngine} will be created to back this + * {@code FlutterFragment}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @Nullable + public FlutterEngine provideFlutterEngine(@NonNull Context context) { + // Defer to the FragmentActivity that owns us to see if it wants to provide a + // FlutterEngine. + FlutterEngine flutterEngine = null; + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterEngineProvider) { + // Defer to the Activity that owns us to provide a FlutterEngine. + Log.d(TAG, "Deferring to attached Activity to provide a FlutterEngine."); + FlutterEngineProvider flutterEngineProvider = (FlutterEngineProvider) attachedActivity; + flutterEngine = flutterEngineProvider.provideFlutterEngine(getContext()); + } + + return flutterEngine; + } + + /** + * Hook for subclasses to obtain a reference to the {@link FlutterEngine} that is owned + * by this {@code FlutterActivity}. + */ + @Nullable + public FlutterEngine getFlutterEngine() { + return delegate.getFlutterEngine(); + } + + @Nullable + @Override + public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { + if (activity != null) { + return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel()); + } else { + return null; + } + } + + /** + * Configures a {@link FlutterEngine} after its creation. + *

+ * 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)}. + *

+ * 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 configuration, e.g., plugins, must not expect or depend upon an available + * {@code Activity} at the time that this method is invoked. + *

+ * The default behavior of this method is to defer to the owning {@code FragmentActivity} + * as a {@link FlutterEngineConfigurator}. Subclasses can override this method if the + * subclass needs to override the {@code FragmentActivity}'s behavior, or add to it. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterEngineConfigurator) { + ((FlutterEngineConfigurator) attachedActivity).configureFlutterEngine(flutterEngine); + } + } + + /** + * See {@link Builder#shouldAttachEngineToActivity()}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + public boolean shouldAttachEngineToActivity() { + return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY); + } + + /** + * Returns true if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive + * the {@code FlutterFragment}, itself. + *

+ * Defaults to false. This method can be overridden in subclasses to retain the + * {@link FlutterEngine}. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + // TODO(mattcarroll): consider a dynamic determination of this preference based on whether the + // engine was created automatically, or if the engine was provided manually. + // Manually provided engines should probably not be destroyed. + @Override + public boolean retainFlutterEngineAfterHostDestruction() { + return false; + } + /** * Invoked after the {@link FlutterView} within this {@code FlutterFragment} renders its first * frame. *

- * The owning {@code Activity} is also sent this message, if it implements - * {@link OnFirstFrameRenderedListener}. This method is invoked before the {@code Activity}'s - * version. + * This method forwards {@code onFirstFrameRendered()} to its attached {@code Activity}, if + * the attached {@code Activity} implements {@link OnFirstFrameRenderedListener}. + *

+ * Subclasses that override this method must call through to the {@code super} method. + *

+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} */ - protected void onFirstFrameRendered() {} - - /** - * Provides a {@link FlutterEngine} instance to be used by a {@code FlutterFragment}. - *

- * {@link FlutterEngine} instances require significant time to warm up. Therefore, a developer - * might choose to hold onto an existing {@link FlutterEngine} and connect it to various - * {@link FlutterActivity}s and/or {@code FlutterFragments}. - *

- * If the {@link FragmentActivity} that owns this {@code FlutterFragment} implements - * {@code FlutterEngineProvider}, that {@link FragmentActivity} will be given an opportunity - * to provide a {@link FlutterEngine} instead of the {@code FlutterFragment} creating a - * new one. The {@link FragmentActivity} can provide an existing, pre-warmed {@link FlutterEngine}, - * if desired. - *

- * See {@link #setupFlutterEngine()} for more information. - */ - public interface FlutterEngineProvider { - /** - * Returns the {@link FlutterEngine} that should be used by a child {@code FlutterFragment}. - *

- * This method may return a new {@link FlutterEngine}, an existing, cached {@link FlutterEngine}, - * or null to express that the {@code FlutterEngineProvider} would like the {@code FlutterFragment} - * to provide its own {@code FlutterEngine} instance. - */ - @Nullable - FlutterEngine provideFlutterEngine(@NonNull Context context); + @Override + public void onFirstFrameRendered() { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof OnFirstFrameRenderedListener) { + ((OnFirstFrameRenderedListener) attachedActivity).onFirstFrameRendered(); + } } /** - * Configures a {@link FlutterEngine} after it is created, e.g., adds plugins. - *

- * This interface may be applied to a {@link FragmentActivity} that owns a {@code FlutterFragment}. + * Annotates methods in {@code FlutterFragment} that must be called by the containing + * {@code Activity}. */ - public interface FlutterEngineConfigurator { - /** - * Configures the given {@link FlutterEngine}. - *

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

- * 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 configuration, e.g., plugins, must not expect or depend upon an available - * {@code Activity} at the time that this method is invoked. - */ - void configureFlutterEngine(@NonNull FlutterEngine flutterEngine); - } + @interface ActivityCallThrough {} - /** - * Provides a {@link SplashScreen} to display while Flutter initializes and renders its first - * frame. - */ - public interface SplashScreenProvider { - /** - * Provides a {@link SplashScreen} to display while Flutter initializes and renders its first - * frame. - */ - @Nullable - SplashScreen provideSplashScreen(); - } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java new file mode 100644 index 00000000000..14b69f98ab8 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java @@ -0,0 +1,20 @@ +// 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 android.support.annotation.Nullable; + +/** + * Provides a {@link SplashScreen} to display while Flutter initializes and renders its first + * frame. + */ +public interface SplashScreenProvider { + /** + * Provides a {@link SplashScreen} to display while Flutter initializes and renders its first + * frame. + */ + @Nullable + SplashScreen provideSplashScreen(); +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 2b8653c49d3..cb9086c756c 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -10,6 +10,7 @@ import android.support.annotation.Nullable; import android.support.annotation.UiThread; import java.nio.ByteBuffer; +import java.util.Objects; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; @@ -283,6 +284,24 @@ public class DartExecutor implements BinaryMessenger { public String toString() { return "DartEntrypoint( bundle path: " + pathToBundle + ", function: " + dartEntrypointFunctionName + " )"; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DartEntrypoint that = (DartEntrypoint) o; + + if (!pathToBundle.equals(that.pathToBundle)) return false; + return dartEntrypointFunctionName.equals(that.dartEntrypointFunctionName); + } + + @Override + public int hashCode() { + int result = pathToBundle.hashCode(); + result = 31 * result + dartEntrypointFunctionName.hashCode(); + return result; + } } /** diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 3ca13ec44a2..bed7648a523 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -25,7 +25,7 @@ import io.flutter.plugin.common.ActivityLifecycleListener; /** * Android implementation of the platform plugin. */ -public class PlatformPlugin implements ActivityLifecycleListener { +public class PlatformPlugin { public static final int DEFAULT_SYSTEM_UI = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; @@ -181,7 +181,15 @@ public class PlatformPlugin implements ActivityLifecycleListener { updateSystemUiOverlays(); } - private void updateSystemUiOverlays(){ + /** + * Refreshes Android's window system UI (AKA system chrome) to match Flutter's desired + * {@link PlatformChannel.SystemChromeStyle}. + *

+ * Updating the system UI Overlays is accomplished by altering the decor view of the + * {@link Window} associated with the {@link Activity} that was provided to this + * {@code PlatformPlugin}. + */ + public void updateSystemUiOverlays(){ activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays); if (currentTheme != null) { setSystemChromeSystemUIOverlayStyle(currentTheme); @@ -263,9 +271,4 @@ public class PlatformPlugin implements ActivityLifecycleListener { ClipData clip = ClipData.newPlainText("text label?", text); clipboard.setPrimaryClip(clip); } - - @Override - public void onPostResume() { - updateSystemUiOverlays(); - } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterMain.java b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterMain.java index 58aed200628..9552dafa013 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterMain.java @@ -14,6 +14,7 @@ import android.os.Looper; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.WindowManager; @@ -22,7 +23,6 @@ import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; import java.io.File; -import java.io.IOException; import java.util.*; /** @@ -56,6 +56,13 @@ public class FlutterMain { private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + private static boolean isRunningInRobolectricTest = false; + + @VisibleForTesting + public static void setIsRunningInRobolectricTest(boolean isRunningInRobolectricTest) { + FlutterMain.isRunningInRobolectricTest = isRunningInRobolectricTest; + } + @NonNull private static String fromFlutterAssets(@NonNull String filePath) { return sFlutterAssetsDir + File.separator + filePath; @@ -96,6 +103,10 @@ public class FlutterMain { * @param applicationContext The Android application context. */ public static void startInitialization(@NonNull Context applicationContext) { + // Do nothing if we're running this in a Robolectric test. + if (isRunningInRobolectricTest) { + return; + } startInitialization(applicationContext, new Settings()); } @@ -105,6 +116,11 @@ public class FlutterMain { * @param settings Configuration settings. */ public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) { + // Do nothing if we're running this in a Robolectric test. + if (isRunningInRobolectricTest) { + return; + } + if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("startInitialization must be called on the main thread"); } @@ -140,6 +156,11 @@ public class FlutterMain { * @param args Flags sent to the Flutter runtime. */ public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { + // Do nothing if we're running this in a Robolectric test. + if (isRunningInRobolectricTest) { + return; + } + if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); } @@ -207,6 +228,11 @@ public class FlutterMain { @NonNull Handler callbackHandler, @NonNull Runnable callback ) { + // Do nothing if we're running this in a Robolectric test. + if (isRunningInRobolectricTest) { + return; + } + if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java index dff7a5e2d8d..6f327d7e4a9 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java @@ -199,7 +199,12 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture // Create and setup plugins PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel); - addActivityLifecycleListener(platformPlugin); + addActivityLifecycleListener(new ActivityLifecycleListener() { + @Override + public void onPostResume() { + platformPlugin.updateSystemUiOverlays(); + } + }); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = new TextInputPlugin(this, dartExecutor, platformViewsController); diff --git a/engine/src/flutter/shell/platform/android/test/README.md b/engine/src/flutter/shell/platform/android/test/README.md index ac6edc64299..8d72cef9396 100644 --- a/engine/src/flutter/shell/platform/android/test/README.md +++ b/engine/src/flutter/shell/platform/android/test/README.md @@ -62,10 +62,15 @@ to use as the base for the pre-existing package. Add new dependencies to `lib/`. Once you've uploaded the new version, also make sure to tag it with the updated timestamp and robolectric version (most likely still 3.8, unless you've migrated -all the packges to 4+). +all the packages to 4+). - $ cipd set-tag --version= -tag "last_updated:" - $ cipd set-tag --version= -tag "robolectric_version:" + $ cipd set-tag flutter/android/robolectric --version= -tag=last_updated: + +Example of a last-updated timestamp: 2019-07-29T15:27:42-0700 + +You can generate the same date format with `date +%Y-%m-%dT%T%z`. + + $ cipd set-tag flutter/android/robolectric --version= -tag=robolectric_version: You can run `cipd describe flutter/android/robolectric_bundle --version=` to verify. You should see: @@ -79,8 +84,8 @@ Tags: robolectric_version: ``` -Then update the `DEPS` file to use the new version by pointing to your new -`last_updated_at` tag. +Then update the `DEPS` file (located at /src/flutter/DEPS) to use the new version by pointing to +your new `last_updated_at` tag. ``` 'src/third_party/robolectric': { diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/engine/src/flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 9b705f80fad..23b9a6010ff 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -6,6 +6,7 @@ package io.flutter; import io.flutter.SmokeTest; import io.flutter.util.PreconditionsTest; +import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -14,7 +15,8 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ PreconditionsTest.class, - SmokeTest.class + SmokeTest.class, + FlutterActivityAndFragmentDelegateTest.class, }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ public class FlutterTestSuite {} diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java new file mode 100644 index 00000000000..8e0e1ea88f5 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -0,0 +1,502 @@ +package io.flutter.embedding.android; + +import android.app.Activity; +import android.arch.lifecycle.Lifecycle; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.FlutterShellArgs; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.LifecycleChannel; +import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.embedding.engine.systemchannels.NavigationChannel; +import io.flutter.embedding.engine.systemchannels.SettingsChannel; +import io.flutter.embedding.engine.systemchannels.SystemChannel; +import io.flutter.plugin.platform.PlatformPlugin; +import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.view.FlutterMain; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +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.when; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterActivityAndFragmentDelegateTest { + private FlutterEngine mockFlutterEngine; + private FakeHost fakeHost; + private FakeHost spyHost; + + @Before + public void setup() { + // FlutterMain is utilized statically, therefore we need to inform it to behave differently + // for testing purposes. + FlutterMain.setIsRunningInRobolectricTest(true); + + // Create a mocked FlutterEngine for the various interactions required by the delegate + // being tested. + mockFlutterEngine = mockFlutterEngine(); + + // Create a fake Host, which is required by the delegate being tested. + fakeHost = new FakeHost(); + fakeHost.flutterEngine = mockFlutterEngine; + + // Create a spy around the FakeHost so that we can verify method invocations. + spyHost = spy(fakeHost); + } + + @After + public void teardown() { + // Return FlutterMain to normal. + FlutterMain.setIsRunningInRobolectricTest(false); + } + + @Test + public void itSendsLifecycleEventsToFlutter() { + // ---- Test setup ---- + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + + // We're testing lifecycle behaviors, which require/expect that certain methods have already + // been executed by the time they run. Therefore, we run those expected methods first. + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + + // --- Execute the behavior under test --- + // By the time an Activity/Fragment is started, we don't expect any lifecycle messages + // to have been sent to Flutter. + delegate.onStart(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + + // When the Activity/Fragment is resumed, a resumed message should have been sent to Flutter. + delegate.onResume(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + + // When the Activity/Fragment is paused, an inactive message should have been sent to Flutter. + delegate.onPause(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + + // When the Activity/Fragment is stopped, a paused message should have been sent to Flutter. + // Notice that Flutter uses the term "paused" in a different way, and at a different time + // than the Android OS. + delegate.onStop(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); + } + + @Test + public void itDefersToTheHostToProvideFlutterEngine() { + // ---- Test setup ---- + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is created in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Verify that the host was asked to provide a FlutterEngine. + verify(spyHost, times(1)).provideFlutterEngine(any(Context.class)); + + // Verify that the delegate's FlutterEngine is our mock FlutterEngine. + assertEquals("The delegate failed to use the host's FlutterEngine.", mockFlutterEngine, delegate.getFlutterEngine()); + } + + @Test + public void itGivesHostAnOpportunityToConfigureFlutterEngine() { + // ---- Test setup ---- + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is created in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Verify that the host was asked to configure our FlutterEngine. + verify(spyHost, times(1)).configureFlutterEngine(mockFlutterEngine); + } + + @Test + public void itSendsInitialRouteToFlutter() { + // ---- Test setup ---- + // Set initial route on our fake Host. + spyHost.initialRoute = "/my/route"; + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The initial route is sent in onStart(). + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + + // Verify that the navigation channel was given our initial route. + verify(mockFlutterEngine.getNavigationChannel(), times(1)).setInitialRoute("/my/route"); + } + + @Test + public void itExecutesDartEntrypointProvidedByHost() { + // ---- Test setup ---- + // Set Dart entrypoint parameters on fake host. + spyHost.appBundlePath = "/my/bundle/path"; + spyHost.dartEntrypointFunctionName = "myEntrypoint"; + + // Create the DartEntrypoint that we expect to be executed. + DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint( + RuntimeEnvironment.application.getAssets(), + "/my/bundle/path", + "myEntrypoint" + ); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // Dart is executed in onStart(). + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + + // Verify that the host's Dart entrypoint was used. + verify(mockFlutterEngine.getDartExecutor(), times(1)).executeDartEntrypoint(eq(dartEntrypoint)); + } + + // "Attaching" to the surrounding Activity refers to Flutter being able to control + // system chrome and other Activity-level details. If Flutter is not attached to the + // surrounding Activity, it cannot control those details. This includes plugins. + @Test + public void itAttachesFlutterToTheActivityIfDesired() { + // ---- Test setup ---- + // Declare that the host wants Flutter to attach to the surrounding Activity. + spyHost.shouldAttachToActivity = true; + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // Flutter is attached to the surrounding Activity in onAttach. + delegate.onAttach(RuntimeEnvironment.application); + + // Verify that the ActivityControlSurface was told to attach to an Activity. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).attachToActivity(any(Activity.class), any(Lifecycle.class)); + + // Flutter is detached from the surrounding Activity in onDetach. + delegate.onDetach(); + + // Verify that the ActivityControlSurface was told to detach from the Activity. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).detachFromActivity(); + } + + // "Attaching" to the surrounding Activity refers to Flutter being able to control + // system chrome and other Activity-level details. If Flutter is not attached to the + // surrounding Activity, it cannot control those details. This includes plugins. + @Test + public void itDoesNotAttachFlutterToTheActivityIfNotDesired() { + // ---- Test setup ---- + // Declare that the host does NOT want Flutter to attach to the surrounding Activity. + spyHost.shouldAttachToActivity = false; + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // Flutter is attached to the surrounding Activity in onAttach. + delegate.onAttach(RuntimeEnvironment.application); + + // Verify that the ActivityControlSurface was NOT told to attach to an Activity. + verify(mockFlutterEngine.getActivityControlSurface(), never()).attachToActivity(any(Activity.class), any(Lifecycle.class)); + + // Flutter is detached from the surrounding Activity in onDetach. + delegate.onDetach(); + + // Verify that the ActivityControlSurface was NOT told to detach from the Activity. + verify(mockFlutterEngine.getActivityControlSurface(), never()).detachFromActivity(); + } + + @Test + public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and inform our delegate that the back button was pressed. + delegate.onBackPressed(); + + // Verify that the navigation channel tried to send a message to Flutter. + verify(mockFlutterEngine.getNavigationChannel(), times(1)).popRoute(); + } + + @Test + public void itForwardsOnRequestPermissionsResultToFlutterEngine() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onRequestPermissionsResult(0, new String[]{}, new int[]{}); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onRequestPermissionsResult(any(Integer.class), any(String[].class), any(int[].class)); + } + + @Test + public void itForwardsOnNewIntentToFlutterEngine() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onNewIntent(mock(Intent.class)); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onNewIntent(any(Intent.class)); + } + + @Test + public void itForwardsOnActivityResultToFlutterEngine() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onActivityResult(0, 0, null); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onActivityResult(any(Integer.class), any(Integer.class), any(Intent.class)); + } + + @Test + public void itForwardsOnUserLeaveHintToFlutterEngine() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onUserLeaveHint(); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onUserLeaveHint(); + } + + @Test + public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onTrimMemory(TRIM_MEMORY_RUNNING_LOW); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning(); + } + + @Test + public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() { + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is setup in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + + // Emulate the host and call the method that we expect to be forwarded. + delegate.onLowMemory(); + + // Verify that the call was forwarded to the engine. + verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning(); + } + + /** + * Creates a mock {@link FlutterEngine}. + *

+ * The heuristic for deciding what to mock in the given {@link FlutterEngine} is that we + * should mock the minimum number of necessary methods and associated objects. Maintaining + * developers should add more mock behavior as required for tests, but should avoid mocking + * things that are not required for the correct execution of tests. + */ + @NonNull + private FlutterEngine mockFlutterEngine() { + // The use of SettingsChannel by the delegate requires some behavior of its own, so it is + // explicitly mocked with some internal behavior. + SettingsChannel fakeSettingsChannel = mock(SettingsChannel.class); + SettingsChannel.MessageBuilder fakeMessageBuilder = mock(SettingsChannel.MessageBuilder.class); + when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class))).thenReturn(fakeMessageBuilder); + when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder); + when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder); + when(fakeSettingsChannel.startMessage()).thenReturn(fakeMessageBuilder); + + // Mock FlutterEngine and all of its required direct calls. + FlutterEngine engine = mock(FlutterEngine.class); + when(engine.getDartExecutor()).thenReturn(mock(DartExecutor.class)); + when(engine.getRenderer()).thenReturn(mock(FlutterRenderer.class)); + when(engine.getPlatformViewsController()).thenReturn(mock(PlatformViewsController.class)); + when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class)); + when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel); + when(engine.getLocalizationChannel()).thenReturn(mock(LocalizationChannel.class)); + when(engine.getLifecycleChannel()).thenReturn(mock(LifecycleChannel.class)); + when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class)); + when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class)); + when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class)); + + return engine; + } + + /** + * A {@link FlutterActivityAndFragmentDelegate.Host} that returns values desired by this + * test suite. + *

+ * Sane defaults are set for all properties. Tests in this suite can alter {@code FakeHost} + * properties as needed for each test. + */ + private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host { + private FlutterEngine flutterEngine; + private String initialRoute = null; + private String appBundlePath = "fake/path/"; + private String dartEntrypointFunctionName = "main"; + private Activity activity; + private boolean shouldAttachToActivity = false; + private boolean retainFlutterEngine = false; + + @NonNull + @Override + public Context getContext() { + return RuntimeEnvironment.application; + } + + @Nullable + @Override + public Activity getActivity() { + if (activity == null) { + // We must provide a real (or close to real) Activity because it is passed to + // the FlutterView that the delegate instantiates. + activity = Robolectric.setupActivity(Activity.class); + } + + return activity; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mock(Lifecycle.class); + } + + @NonNull + @Override + public FlutterShellArgs getFlutterShellArgs() { + return new FlutterShellArgs(new String[]{}); + } + + @NonNull + @Override + public String getDartEntrypointFunctionName() { + return dartEntrypointFunctionName; + } + + @NonNull + @Override + public String getAppBundlePath() { + return appBundlePath; + } + + @Nullable + @Override + public String getInitialRoute() { + return initialRoute; + } + + @NonNull + @Override + public FlutterView.RenderMode getRenderMode() { + return FlutterView.RenderMode.surface; + } + + @NonNull + @Override + public FlutterView.TransparencyMode getTransparencyMode() { + return FlutterView.TransparencyMode.opaque; + } + + @Nullable + @Override + public SplashScreen provideSplashScreen() { + return null; + } + + @Nullable + @Override + public FlutterEngine provideFlutterEngine(@NonNull Context context) { + return flutterEngine; + } + + @Nullable + @Override + public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { + return null; + } + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {} + + @Override + public boolean shouldAttachEngineToActivity() { + return shouldAttachToActivity; + } + + @Override + public boolean retainFlutterEngineAfterHostDestruction() { + return retainFlutterEngine; + } + + @Override + public void onFirstFrameRendered() {} + } +} diff --git a/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java b/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java index 9b79fa9fc03..d1632efa79f 100644 --- a/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java +++ b/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java @@ -45,7 +45,9 @@ public class MainActivity extends FlutterActivity implements OnFirstFrameRendere } } - private FlutterShellArgs getFlutterShellArgs() { + @Override + @NonNull + public FlutterShellArgs getFlutterShellArgs() { FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); args.add(FlutterShellArgs.ARG_TRACE_STARTUP); args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); @@ -54,20 +56,6 @@ public class MainActivity extends FlutterActivity implements OnFirstFrameRendere return args; } - @Override - @NonNull - protected FlutterFragment createFlutterFragment() { - return new FlutterFragment.Builder() - .dartEntrypoint(getDartEntrypoint()) - .initialRoute(getInitialRoute()) - .appBundlePath(getAppBundlePath()) - .flutterShellArgs(getFlutterShellArgs()) - .renderMode(FlutterView.RenderMode.surface) - .transparencyMode(FlutterView.TransparencyMode.opaque) - .shouldAttachEngineToActivity(true) - .build(); - } - private void writeTimelineData(Uri logFile) { if (logFile == null) { throw new IllegalArgumentException();