From 25c155915811a2aef58ffff3ec1e9d29e8267a36 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 13 Sep 2019 15:16:22 -0700 Subject: [PATCH 1/3] Introduced FlutterFragmentActivity. --- ci/licenses_golden/licenses_flutter | 2 + shell/platform/android/BUILD.gn | 2 + .../embedding/android/FlutterActivity.java | 57 +- .../android/FlutterActivityLaunchConfigs.java | 34 + .../android/FlutterFragmentActivity.java | 671 ++++++++++++++++++ .../android/FlutterActivityTest.java | 6 +- 6 files changed, 735 insertions(+), 37 deletions(-) create mode 100644 shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java create mode 100644 shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index dbb8643280c0d..cb05b04cbbb6a 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -567,9 +567,11 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Andro 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/FlutterActivityLaunchConfigs.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/FlutterFragmentActivity.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 diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 522fafd631b74..b273cc773caa4 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -135,9 +135,11 @@ action("flutter_shell_java") { "io/flutter/embedding/android/DrawableSplashScreen.java", "io/flutter/embedding/android/FlutterActivity.java", "io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java", + "io/flutter/embedding/android/FlutterActivityLaunchConfigs.java", "io/flutter/embedding/android/FlutterEngineConfigurator.java", "io/flutter/embedding/android/FlutterEngineProvider.java", "io/flutter/embedding/android/FlutterFragment.java", + "io/flutter/embedding/android/FlutterFragmentActivity.java", "io/flutter/embedding/android/FlutterSplashView.java", "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 396ad89a407ac..5a505de678cc3 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -25,12 +25,25 @@ import android.view.WindowManager; import io.flutter.Log; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; 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; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.SPLASH_SCREEN_META_DATA_KEY; + /** * {@code Activity} which displays a fullscreen Flutter UI. *

@@ -45,7 +58,7 @@ *

* The Flutter route that is initially loaded within this {@code Activity} is "/". The initial * 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". + * {@link FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link". *

* The initial route can each be controlled using a {@link NewEngineIntentBuilder} via * {@link NewEngineIntentBuilder#initialRoute}. @@ -180,23 +193,6 @@ public class FlutterActivity extends Activity LifecycleOwner { private static final String TAG = "FlutterActivity"; - // Meta-data arguments, processed from manifest XML. - protected static final String DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint"; - protected static final String INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute"; - protected static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable"; - protected static final String NORMAL_THEME_META_DATA_KEY = "io.flutter.embedding.android.NormalTheme"; - - // Intent extra arguments. - protected static final String EXTRA_INITIAL_ROUTE = "initial_route"; - protected static final String EXTRA_BACKGROUND_MODE = "background_mode"; - protected static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; - protected static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; - - // Default configuration. - protected static final String DEFAULT_DART_ENTRYPOINT = "main"; - protected static final String DEFAULT_INITIAL_ROUTE = "/"; - protected static final String DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque.name(); - /** * 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. @@ -460,8 +456,8 @@ public SplashScreen provideSplashScreen() { * Returns a {@link Drawable} to be used as a splash screen as requested by meta-data in the * {@code AndroidManifest.xml} file, or null if no such splash screen is requested. *

- * See {@link #SPLASH_SCREEN_META_DATA_KEY} for the meta-data key to be used in a - * manifest file. + * See {@link FlutterActivityLaunchConfigs#SPLASH_SCREEN_META_DATA_KEY} for the meta-data key to + * be used in a manifest file. */ @Nullable @SuppressWarnings("deprecation") @@ -670,8 +666,8 @@ public boolean shouldDestroyEngineWithHost() { * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. *

* This preference can be controlled by setting a {@code } called - * {@link #DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest definition for this - * {@code FlutterActivity}. + * {@link FlutterActivityLaunchConfigs#DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest + * definition for this {@code FlutterActivity}. *

* Subclasses may override this method to directly control the Dart entrypoint. */ @@ -695,9 +691,11 @@ public String getDartEntrypointFunctionName() { *

* This preference can be controlled with 2 methods: *

    - *
  1. Pass a boolean as {@link #EXTRA_INITIAL_ROUTE} with the launching {@code Intent}, or
  2. - *
  3. Set a {@code } called {@link #INITIAL_ROUTE_META_DATA_KEY} for this - * {@code Activity} in the Android manifest.
  4. + *
  5. Pass a boolean as {@link FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE} with the + * launching {@code Intent}, or
  6. + *
  7. Set a {@code } called + * {@link FlutterActivityLaunchConfigs#INITIAL_ROUTE_META_DATA_KEY} for this {@code Activity} + * in the Android manifest.
  8. *
* If both preferences are set, the {@code Intent} preference takes priority. *

@@ -899,13 +897,4 @@ public void onFlutterUiNoLongerDisplayed() { // no-op } - /** - * The mode of the background of a {@code FlutterActivity}, either opaque or transparent. - */ - public enum BackgroundMode { - /** Indicates a FlutterActivity with an opaque background. This is the default. */ - opaque, - /** Indicates a FlutterActivity with a transparent background. */ - transparent - } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java new file mode 100644 index 0000000000000..6634ce7c3ec24 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java @@ -0,0 +1,34 @@ +// 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; + +class FlutterActivityLaunchConfigs { + // Meta-data arguments, processed from manifest XML. + static final String DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint"; + static final String INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute"; + static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable"; + static final String NORMAL_THEME_META_DATA_KEY = "io.flutter.embedding.android.NormalTheme"; + + // Intent extra arguments. + static final String EXTRA_INITIAL_ROUTE = "initial_route"; + static final String EXTRA_BACKGROUND_MODE = "background_mode"; + static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; + + // Default configuration. + static final String DEFAULT_DART_ENTRYPOINT = "main"; + static final String DEFAULT_INITIAL_ROUTE = "/"; + static final String DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque.name(); + + /** + * The mode of the background of a Flutter {@code Activity}, either opaque or transparent. + */ + public enum BackgroundMode { + /** Indicates a FlutterActivity with an opaque background. This is the default. */ + opaque, + /** Indicates a FlutterActivity with a transparent background. */ + transparent + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java new file mode 100644 index 0000000000000..7b27ce9713275 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -0,0 +1,671 @@ +// 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.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +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.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.android.FlutterActivityLaunchConfigs.BackgroundMode; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.plugin.platform.PlatformPlugin; +import io.flutter.view.FlutterMain; + +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.INITIAL_ROUTE_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.NORMAL_THEME_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.SPLASH_SCREEN_META_DATA_KEY; + +/** + * A Flutter {@code Activity} that is based upon {@link FragmentActivity}. + *

+ * {@code FlutterFragmentActivity} exists because there are some Android APIs + * in the ecosystem that only accept a {@link FragmentActivity}. If a + * {@link FragmentActivity} is not required, you should consider using a + * regular {@link FlutterActivity} instead, because {@link FlutterActivity} + * is considered to be the standard, canonical implementation of a Flutter + * {@code Activity}. + */ +public class FlutterFragmentActivity extends FragmentActivity + implements SplashScreenProvider, + FlutterEngineProvider, + FlutterEngineConfigurator { + private static final String TAG = "FlutterFragmentActivity"; + + // 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 + + /** + * Creates an {@link Intent} that launches a {@code FlutterFragmentActivity}, which executes + * a {@code main()} Dart entrypoint, and displays the "/" route as Flutter's initial route. + */ + @NonNull + public static Intent createDefaultIntent(@NonNull Context launchContext) { + return withNewEngine().build(launchContext); + } + + /** + * Creates an {@link FlutterFragmentActivity.NewEngineIntentBuilder}, which can be used to + * configure an {@link Intent} to launch a {@code FlutterActivity} that internally creates a new + * {@link FlutterEngine} using the desired Dart entrypoint, initial route, etc. + */ + @NonNull + public static NewEngineIntentBuilder withNewEngine() { + return new NewEngineIntentBuilder(FlutterFragmentActivity.class); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterFragmentActivity} with a new + * {@link FlutterEngine} and the desired configuration. + */ + public static class NewEngineIntentBuilder { + private final Class activityClass; + private String initialRoute = DEFAULT_INITIAL_ROUTE; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of + * {@code FlutterFragmentActivity}. + *

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

+ * {@code + * return new NewEngineIntentBuilder(MyFlutterActivity.class); + * } + */ + protected NewEngineIntentBuilder(@NonNull Class activityClass) { + this.activityClass = activityClass; + } + + /** + * The initial route that a Flutter app will render in this {@code FlutterFragmentActivity}, + * defaults to "/". + */ + @NonNull + public NewEngineIntentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * The mode of {@code FlutterFragmentActivity}'s background, either + * {@link BackgroundMode#opaque} or {@link BackgroundMode#transparent}. + *

+ * The default background mode is {@link BackgroundMode#opaque}. + *

+ * Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner + * {@link FlutterView} of this {@code FlutterFragmentActivity} to be configured with a + * {@link FlutterTextureView} to support transparency. This choice has a non-trivial performance + * impact. A transparent background should only be used if it is necessary for the app design + * being implemented. + *

+ * A {@code FlutterFragmentActivity} that is configured with a background mode of + * {@link BackgroundMode#transparent} must have a theme applied to it that includes the + * following property: {@code true}. + */ + @NonNull + public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + /** + * Creates and returns an {@link Intent} that will launch a {@code FlutterFragmentActivity} with + * the desired configuration. + */ + @NonNull + public Intent build(@NonNull Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_INITIAL_ROUTE, initialRoute) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true); + } + } + + /** + * Creates a {@link CachedEngineIntentBuilder}, which can be used to configure an {@link Intent} + * to launch a {@code FlutterFragmentActivity} that internally uses an existing + * {@link FlutterEngine} that is cached in {@link io.flutter.embedding.engine.FlutterEngineCache}. + */ + @NonNull + public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) { + return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterFragmentActivity} with an + * existing {@link FlutterEngine} that is cached in + * {@link io.flutter.embedding.engine.FlutterEngineCache}. + */ + public static class CachedEngineIntentBuilder { + private final Class activityClass; + private final String cachedEngineId; + private boolean destroyEngineWithActivity = false; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code CachedEngineIntentBuilder} to be used by subclasses of + * {@code FlutterFragmentActivity}. + *

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

+ * {@code + * return new CachedEngineIntentBuilder(MyFlutterActivity.class, engineId); + * } + */ + protected CachedEngineIntentBuilder( + @NonNull Class activityClass, + @NonNull String engineId + ) { + this.activityClass = activityClass; + this.cachedEngineId = engineId; + } + + /** + * Returns true if the cached {@link FlutterEngine} should be destroyed and removed from the + * cache when this {@code FlutterFragmentActivity} is destroyed. + *

+ * The default value is {@code false}. + */ + public CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) { + this.destroyEngineWithActivity = destroyEngineWithActivity; + return this; + } + + /** + * The mode of {@code FlutterFragmentActivity}'s background, either + * {@link BackgroundMode#opaque} or {@link BackgroundMode#transparent}. + *

+ * The default background mode is {@link BackgroundMode#opaque}. + *

+ * Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner + * {@link FlutterView} of this {@code FlutterFragmentActivity} to be configured with a + * {@link FlutterTextureView} to support transparency. This choice has a non-trivial performance + * impact. A transparent background should only be used if it is necessary for the app design + * being implemented. + *

+ * A {@code FlutterFragmentActivity} that is configured with a background mode of + * {@link BackgroundMode#transparent} must have a theme applied to it that includes the + * following property: {@code true}. + */ + @NonNull + public CachedEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + /** + * Creates and returns an {@link Intent} that will launch a {@code FlutterFragmentActivity} with + * the desired configuration. + */ + @NonNull + public Intent build(@NonNull Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_CACHED_ENGINE_ID, cachedEngineId) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode); + } + } + + @Nullable + private FlutterFragment flutterFragment; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + switchLaunchThemeForNormalTheme(); + + super.onCreate(savedInstanceState); + + configureWindowForTransparency(); + setContentView(createFragmentContainer()); + configureStatusBarForFullscreenFlutterExperience(); + ensureFlutterFragmentCreated(); + } + + /** + * Switches themes for this {@code Activity} from the theme used to launch this + * {@code Activity} to a "normal theme" that is intended for regular {@code Activity} + * operation. + *

+ * This behavior is offered so that a "launch screen" can be displayed while the + * application initially loads. To utilize this behavior in an app, do the following: + *

    + *
  1. Create 2 different themes in style.xml: one theme for the launch screen and + * one theme for normal display. + *
  2. In the launch screen theme, set the "windowBackground" property to a {@code Drawable} + * of your choice. + *
  3. In the normal theme, customize however you'd like. + *
  4. In the AndroidManifest.xml, set the theme of your {@code FlutterFragmentActivity} to + * your launch theme. + *
  5. Add a {@code } property to your {@code FlutterFragmentActivity} with a name + * of "io.flutter.embedding.android.NormalTheme" and set the resource to your normal + * theme, e.g., {@code android:resource="@style/MyNormalTheme}. + *
+ * With the above settings, your launch theme will be used when loading the app, and + * then the theme will be switched to your normal theme once the app has initialized. + *

+ * Do not change aspects of system chrome between a launch theme and normal theme. Either define + * both themes to be fullscreen or not, and define both themes to display the same status bar and + * navigation bar settings. If you wish to adjust system chrome once your Flutter app renders, use + * platform channels to instruct Android to do so at the appropriate time. This will avoid any + * jarring visual changes during app startup. + */ + private void switchLaunchThemeForNormalTheme() { + try { + ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA); + if (activityInfo.metaData != null) { + int normalThemeRID = activityInfo.metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1); + if (normalThemeRID != -1) { + setTheme(normalThemeRID); + } + } else { + Log.d(TAG, "Using the launch theme as normal theme."); + } + } catch (PackageManager.NameNotFoundException exception) { + Log.e(TAG, "Could not read meta-data for FlutterActivity. Using the launch theme as normal theme."); + } + } + + @Nullable + @Override + public SplashScreen provideSplashScreen() { + Drawable manifestSplashDrawable = getSplashScreenFromManifest(); + if (manifestSplashDrawable != null) { + return new DrawableSplashScreen(manifestSplashDrawable); + } else { + return null; + } + } + + /** + * Returns a {@link Drawable} to be used as a splash screen as requested by meta-data in the + * {@code AndroidManifest.xml} file, or null if no such splash screen is requested. + *

+ * See {@link FlutterActivityLaunchConfigs#SPLASH_SCREEN_META_DATA_KEY} for the meta-data key to + * be used in a manifest file. + */ + @Nullable + @SuppressWarnings("deprecation") + private Drawable getSplashScreenFromManifest() { + try { + ActivityInfo activityInfo = getPackageManager().getActivityInfo( + getComponentName(), + PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES + ); + Bundle metadata = activityInfo.metaData; + Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : 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; + } + } + + /** + * Sets this {@code Activity}'s {@code Window} background to be transparent, and hides the status + * bar, if this {@code Activity}'s desired {@link BackgroundMode} is {@link BackgroundMode#transparent}. + *

+ * For {@code Activity} transparency to work as expected, the theme applied to this {@code Activity} + * must include {@code true}. + */ + private void configureWindowForTransparency() { + BackgroundMode backgroundMode = getBackgroundMode(); + if (backgroundMode == BackgroundMode.transparent) { + getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ); + } + } + + /** + * Creates a {@link FrameLayout} with an ID of {@code #FRAGMENT_CONTAINER_ID} that will contain + * the {@link FlutterFragment} displayed by this {@code FlutterFragmentActivity}. + *

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

+ * If no {@link FlutterFragment} exists in this {@code FlutterFragmentActivity}, then a + * {@link FlutterFragment} is created and added. If a {@link FlutterFragment} does exist in this + * {@code FlutterFragmentActivity}, 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 FlutterFragmentActivity} + * displays. + *

+ * Subclasses may override this method to return a specialization of {@link FlutterFragment}. + */ + @NonNull + protected FlutterFragment createFlutterFragment() { + BackgroundMode backgroundMode = getBackgroundMode(); + FlutterView.RenderMode renderMode = backgroundMode == BackgroundMode.opaque + ? FlutterView.RenderMode.surface + : FlutterView.RenderMode.texture; + FlutterView.TransparencyMode transparencyMode = backgroundMode == BackgroundMode.opaque + ? FlutterView.TransparencyMode.opaque + : FlutterView.TransparencyMode.transparent; + + if (getCachedEngineId() != null) { + Log.d(TAG, "Creating FlutterFragment with cached engine:\n" + + "Cached engine ID: " + getCachedEngineId() + "\n" + + "Will destroy engine when Activity is destroyed: " + shouldDestroyEngineWithHost() + "\n" + + "Background transparency mode: " + backgroundMode + "\n" + + "Will attach FlutterEngine to Activity: " + shouldAttachEngineToActivity()); + + return FlutterFragment.withCachedEngine(getCachedEngineId()) + .renderMode(renderMode) + .transparencyMode(transparencyMode) + .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) + .destroyEngineWithFragment(shouldDestroyEngineWithHost()) + .build(); + } else { + Log.d(TAG, "Creating FlutterFragment with new engine:\n" + + "Background transparency mode: " + backgroundMode + "\n" + + "Dart entrypoint: " + getDartEntrypointFunctionName() + "\n" + + "Initial route: " + getInitialRoute() + "\n" + + "App bundle path: " + getAppBundlePath() + "\n" + + "Will attach FlutterEngine to Activity: " + shouldAttachEngineToActivity()); + + return FlutterFragment.withNewEngine() + .dartEntrypoint(getDartEntrypointFunctionName()) + .initialRoute(getInitialRoute()) + .appBundlePath(getAppBundlePath()) + .flutterShellArgs(FlutterShellArgs.fromIntent(getIntent())) + .renderMode(renderMode) + .transparencyMode(transparencyMode) + .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) + .build(); + } + } + + private void configureStatusBarForFullscreenFlutterExperience() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(0x40000000); + window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); + } + } + + @Override + public void onPostResume() { + super.onPostResume(); + flutterFragment.onPostResume(); + } + + @Override + protected void onNewIntent(@NonNull Intent intent) { + // Forward Intents to our FlutterFragment in case it cares. + flutterFragment.onNewIntent(intent); + super.onNewIntent(intent); + } + + @Override + public void onBackPressed() { + flutterFragment.onBackPressed(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + flutterFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onUserLeaveHint() { + flutterFragment.onUserLeaveHint(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + flutterFragment.onTrimMemory(level); + } + + @SuppressWarnings("unused") + @Nullable + protected FlutterEngine getFlutterEngine() { + return flutterFragment.getFlutterEngine(); + } + + /** + * Returns false if the {@link FlutterEngine} backing this {@code FlutterFragmentActivity} should + * outlive this {@code FlutterFragmentActivity}, or true to be destroyed when the + * {@code FlutterFragmentActivity} is destroyed. + *

+ * The default value is {@code true} in cases where {@code FlutterFragmentActivity} created its + * own {@link FlutterEngine}, and {@code false} in cases where a cached {@link FlutterEngine} was + * provided. + */ + public boolean shouldDestroyEngineWithHost() { + return getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false); + } + + /** + * 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.NewEngineFragmentBuilder#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 FlutterFragmentActivity} 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; + } + + /** + * 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. + } + + /** + * The path to the bundle that contains this Flutter app's resources, e.g., Dart code snapshots. + *

+ * When this {@code FlutterFragmentActivity} 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 + 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(); + } + + /** + * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. + *

+ * This preference can be controlled by setting a {@code } called + * {@link FlutterActivityLaunchConfigs#DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest + * definition for this {@code FlutterActivity}. + *

+ * Subclasses may override this method to directly control the Dart entrypoint. + */ + @NonNull + public String getDartEntrypointFunctionName() { + try { + ActivityInfo activityInfo = getPackageManager().getActivityInfo( + getComponentName(), + PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES + ); + Bundle metadata = activityInfo.metaData; + String desiredDartEntrypoint = metadata != null ? metadata.getString(DART_ENTRYPOINT_META_DATA_KEY) : null; + return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT; + } catch (PackageManager.NameNotFoundException e) { + return DEFAULT_DART_ENTRYPOINT; + } + } + + /** + * The initial route that a Flutter app will render upon loading and executing its Dart code. + *

+ * This preference can be controlled with 2 methods: + *

    + *
  1. Pass a boolean as {@link FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE} with the + * launching {@code Intent}, or
  2. + *
  3. Set a {@code } called + * {@link FlutterActivityLaunchConfigs#INITIAL_ROUTE_META_DATA_KEY} for this {@code Activity} + * in the Android manifest.
  4. + *
+ * If both preferences are set, the {@code Intent} preference takes priority. + *

+ * The reason that a {@code } preference is supported is because this {@code Activity} + * might be the very first {@code Activity} launched, which means the developer won't have + * control over the incoming {@code Intent}. + *

+ * Subclasses may override this method to directly control the initial route. + */ + @NonNull + protected String getInitialRoute() { + if (getIntent().hasExtra(EXTRA_INITIAL_ROUTE)) { + return getIntent().getStringExtra(EXTRA_INITIAL_ROUTE); + } + + try { + ActivityInfo activityInfo = getPackageManager().getActivityInfo( + getComponentName(), + PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES + ); + Bundle metadata = activityInfo.metaData; + String desiredInitialRoute = metadata != null ? metadata.getString(INITIAL_ROUTE_META_DATA_KEY) : null; + return desiredInitialRoute != null ? desiredInitialRoute : DEFAULT_INITIAL_ROUTE; + } catch (PackageManager.NameNotFoundException e) { + return DEFAULT_INITIAL_ROUTE; + } + } + + /** + * Returns the ID of a statically cached {@link FlutterEngine} to use within this + * {@code FlutterActivity}, or {@code null} if this {@code FlutterFragmentActivity} does not want + * to use a cached {@link FlutterEngine}. + */ + @Nullable + protected String getCachedEngineId() { + return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_ID); + } + + /** + * The desired window background mode of this {@code Activity}, which defaults to + * {@link BackgroundMode#opaque}. + */ + @NonNull + protected BackgroundMode getBackgroundMode() { + if (getIntent().hasExtra(EXTRA_BACKGROUND_MODE)) { + return BackgroundMode.valueOf(getIntent().getStringExtra(EXTRA_BACKGROUND_MODE)); + } else { + return BackgroundMode.opaque; + } + } + + /** + * 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; + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index 9e6b2985199ed..d4db0b3de6aa3 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -31,7 +31,7 @@ public void itCreatesDefaultIntentWithExpectedDefaults() { assertTrue(flutterActivity.shouldAttachEngineToActivity()); assertNull(flutterActivity.getCachedEngineId()); assertTrue(flutterActivity.shouldDestroyEngineWithHost()); - assertEquals(FlutterActivity.BackgroundMode.opaque, flutterActivity.getBackgroundMode()); + assertEquals(BackgroundMode.opaque, flutterActivity.getBackgroundMode()); assertEquals(FlutterView.RenderMode.surface, flutterActivity.getRenderMode()); assertEquals(FlutterView.TransparencyMode.opaque, flutterActivity.getTransparencyMode()); } @@ -40,7 +40,7 @@ public void itCreatesDefaultIntentWithExpectedDefaults() { public void itCreatesNewEngineIntentWithRequestedSettings() { Intent intent = FlutterActivity.withNewEngine() .initialRoute("/custom/route") - .backgroundMode(FlutterActivity.BackgroundMode.transparent) + .backgroundMode(BackgroundMode.transparent) .build(RuntimeEnvironment.application); ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); FlutterActivity flutterActivity = activityController.get(); @@ -50,7 +50,7 @@ public void itCreatesNewEngineIntentWithRequestedSettings() { assertTrue(flutterActivity.shouldAttachEngineToActivity()); assertNull(flutterActivity.getCachedEngineId()); assertTrue(flutterActivity.shouldDestroyEngineWithHost()); - assertEquals(FlutterActivity.BackgroundMode.transparent, flutterActivity.getBackgroundMode()); + assertEquals(BackgroundMode.transparent, flutterActivity.getBackgroundMode()); assertEquals(FlutterView.RenderMode.texture, flutterActivity.getRenderMode()); assertEquals(FlutterView.TransparencyMode.transparent, flutterActivity.getTransparencyMode()); } From ff89bbb141160312bb19bf033fbb53579befc817 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Mon, 16 Sep 2019 15:51:35 -0700 Subject: [PATCH 2/3] Fixed test compilation issues. --- .../test/io/flutter/embedding/android/FlutterActivityTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index d4db0b3de6aa3..b16673b1dce6c 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; + @Config(manifest=Config.NONE) @RunWith(RobolectricTestRunner.class) public class FlutterActivityTest { From 62d4ed68e7545c8b03edf72571d22973d590222a Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Mon, 16 Sep 2019 23:54:32 -0700 Subject: [PATCH 3/3] Update javadocs. --- .../android/FlutterFragmentActivity.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 7b27ce9713275..136d7cf6fdae9 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -75,7 +75,7 @@ public static Intent createDefaultIntent(@NonNull Context launchContext) { /** * Creates an {@link FlutterFragmentActivity.NewEngineIntentBuilder}, which can be used to - * configure an {@link Intent} to launch a {@code FlutterActivity} that internally creates a new + * configure an {@link Intent} to launch a {@code FlutterFragmentActivity} that internally creates a new * {@link FlutterEngine} using the desired Dart entrypoint, initial route, etc. */ @NonNull @@ -98,7 +98,7 @@ public static class NewEngineIntentBuilder { *

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

* {@code @@ -161,7 +161,7 @@ public Intent build(@NonNull Context context) { */ @NonNull public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) { - return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId); + return new CachedEngineIntentBuilder(FlutterFragmentActivity.class, cachedEngineId); } /** @@ -170,7 +170,7 @@ public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedE * {@link io.flutter.embedding.engine.FlutterEngineCache}. */ public static class CachedEngineIntentBuilder { - private final Class activityClass; + private final Class activityClass; private final String cachedEngineId; private boolean destroyEngineWithActivity = false; private String backgroundMode = DEFAULT_BACKGROUND_MODE; @@ -189,7 +189,7 @@ public static class CachedEngineIntentBuilder { * } */ protected CachedEngineIntentBuilder( - @NonNull Class activityClass, + @NonNull Class activityClass, @NonNull String engineId ) { this.activityClass = activityClass; @@ -297,7 +297,7 @@ private void switchLaunchThemeForNormalTheme() { Log.d(TAG, "Using the launch theme as normal theme."); } } catch (PackageManager.NameNotFoundException exception) { - Log.e(TAG, "Could not read meta-data for FlutterActivity. Using the launch theme as normal theme."); + Log.e(TAG, "Could not read meta-data for FlutterFragmentActivity. Using the launch theme as normal theme."); } } @@ -580,7 +580,7 @@ protected String getAppBundlePath() { *

* This preference can be controlled by setting a {@code } called * {@link FlutterActivityLaunchConfigs#DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest - * definition for this {@code FlutterActivity}. + * definition for this {@code FlutterFragmentActivity}. *

* Subclasses may override this method to directly control the Dart entrypoint. */ @@ -639,7 +639,7 @@ protected String getInitialRoute() { /** * Returns the ID of a statically cached {@link FlutterEngine} to use within this - * {@code FlutterActivity}, or {@code null} if this {@code FlutterFragmentActivity} does not want + * {@code FlutterFragmentActivity}, or {@code null} if this {@code FlutterFragmentActivity} does not want * to use a cached {@link FlutterEngine}. */ @Nullable