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: *
+ * 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 extends FlutterActivity> 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: + *
+ *
+ * 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: + *
+ *
+ * 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: + *
+ *
+ * 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: + *
+ *
+ * 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: *
- * 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=
+ * 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();