diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 802f6b0b67..f03f4dff62 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -471,7 +471,10 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActi FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/AndroidKeyProcessor.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterFragment.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterTextureView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterView.java diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 9b430a0092..fa55b71b96 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -108,7 +108,10 @@ java_library("flutter_shell_java") { "io/flutter/app/FlutterPluginRegistry.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterJNI.java", + "io/flutter/embedding/engine/FlutterShellArgs.java", "io/flutter/embedding/engine/android/AndroidKeyProcessor.java", + "io/flutter/embedding/engine/android/FlutterActivity.java", + "io/flutter/embedding/engine/android/FlutterFragment.java", "io/flutter/embedding/engine/android/FlutterSurfaceView.java", "io/flutter/embedding/engine/android/FlutterTextureView.java", "io/flutter/embedding/engine/android/FlutterView.java", diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java new file mode 100644 index 0000000000..c3b2c9b715 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; + +import java.util.*; + +/** + * Arguments that can be delivered to the Flutter shell when it is created. + *

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

+ * The term "shell" refers to the native code that adapts Flutter to different platforms. Flutter's + * Android Java code initializes a native "shell" and passes these arguments to that native shell + * when it is initialized. See {@link io.flutter.view.FlutterMain#ensureInitializationComplete(Context, String[])} + * for more information. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class FlutterShellArgs { + public static final String ARG_KEY_TRACE_STARTUP = "trace-startup"; + public static final String ARG_TRACE_STARTUP = "--trace-startup"; + public static final String ARG_KEY_START_PAUSED = "start-paused"; + public static final String ARG_START_PAUSED = "--start-paused"; + public static final String ARG_KEY_USE_TEST_FONTS = "use-test-fonts"; + public static final String ARG_USE_TEST_FONTS = "--use-test-fonts"; + public static final String ARG_KEY_ENABLE_DART_PROFILING = "enable-dart-profiling"; + public static final String ARG_ENABLE_DART_PROFILING = "--enable-dart-profiling"; + public static final String ARG_KEY_ENABLE_SOFTWARE_RENDERING = "enable-software-rendering"; + public static final String ARG_ENABLE_SOFTWARE_RENDERING = "--enable-software-rendering"; + public static final String ARG_KEY_SKIA_DETERMINISTIC_RENDERING = "skia-deterministic-rendering"; + public static final String ARG_SKIA_DETERMINISTIC_RENDERING = "--skia-deterministic-rendering"; + public static final String ARG_KEY_TRACE_SKIA = "trace-skia"; + public static final String ARG_TRACE_SKIA = "--trace-skia"; + public static final String ARG_KEY_VERBOSE_LOGGING = "verbose-logging"; + public static final String ARG_VERBOSE_LOGGING = "--verbose-logging"; + + @NonNull + public static FlutterShellArgs fromIntent(@NonNull Intent intent) { + // Before adding more entries to this list, consider that arbitrary + // Android applications can generate intents with extra data and that + // there are many security-sensitive args in the binary. + // TODO(mattcarroll): I left this warning as-is, but we should clarify what exactly this warning is warning against. + ArrayList args = new ArrayList<>(); + + if (intent.getBooleanExtra(ARG_KEY_TRACE_STARTUP, false)) { + args.add(ARG_TRACE_STARTUP); + } + if (intent.getBooleanExtra(ARG_KEY_START_PAUSED, false)) { + args.add(ARG_START_PAUSED); + } + if (intent.getBooleanExtra(ARG_KEY_USE_TEST_FONTS, false)) { + args.add(ARG_USE_TEST_FONTS); + } + if (intent.getBooleanExtra(ARG_KEY_ENABLE_DART_PROFILING, false)) { + args.add(ARG_ENABLE_DART_PROFILING); + } + if (intent.getBooleanExtra(ARG_KEY_ENABLE_SOFTWARE_RENDERING, false)) { + args.add(ARG_ENABLE_SOFTWARE_RENDERING); + } + if (intent.getBooleanExtra(ARG_KEY_SKIA_DETERMINISTIC_RENDERING, false)) { + args.add(ARG_SKIA_DETERMINISTIC_RENDERING); + } + if (intent.getBooleanExtra(ARG_KEY_TRACE_SKIA, false)) { + args.add(ARG_TRACE_SKIA); + } + if (intent.getBooleanExtra(ARG_KEY_VERBOSE_LOGGING, false)) { + args.add(ARG_VERBOSE_LOGGING); + } + + return new FlutterShellArgs(args); + } + + private Set args; + + /** + * Creates a set of Flutter shell arguments from a given {@code String[]} array. + * The given arguments are automatically de-duplicated. + */ + public FlutterShellArgs(@NonNull String[] args) { + this.args = new HashSet<>(Arrays.asList(args)); + } + + /** + * Creates a set of Flutter shell arguments from a given {@code List}. + * The given arguments are automatically de-duplicated. + */ + public FlutterShellArgs(@NonNull List args) { + this.args = new HashSet<>(args); + } + + /** + * Creates a set of Flutter shell arguments from a given {@code Set}. + */ + public FlutterShellArgs(@NonNull Set args) { + this.args = new HashSet<>(args); + } + + /** + * Adds the given {@code arg} to this set of arguments. + * @param arg argument to add + */ + public void add(@NonNull String arg) { + args.add(arg); + } + + /** + * Removes the given {@code arg} from this set of arguments. + * @param arg argument to remove + */ + public void remove(@NonNull String arg) { + args.remove(arg); + } + + /** + * Returns a new {@code String[]} array which contains each of the arguments + * within this {@code FlutterShellArgs}. + * + * @return array of arguments + */ + @NonNull + public String[] toArray() { + String[] argsArray = new String[args.size()]; + return args.toArray(argsArray); + } +} \ No newline at end of file diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterActivity.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterActivity.java new file mode 100644 index 0000000000..182a097f9c --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterActivity.java @@ -0,0 +1,240 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.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.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.widget.FrameLayout; + +import io.flutter.embedding.engine.FlutterShellArgs; +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. + *

+ * 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". + *

+ * 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". + *

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

+ * If Flutter is needed in a location that cannot use an {@code Activity}, consider using + * a {@link FlutterFragment}. Using a {@link FlutterFragment} requires forwarding some calls from + * an {@code Activity} to the {@link FlutterFragment}. + *

+ * If Flutter is needed in a location that can only use a {@code View}, consider using a + * {@link FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an + * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a + * {@code Fragment}. + */ +// TODO(mattcarroll): explain each call forwarded to Fragment (first requires resolution of PluginRegistry API). +public class FlutterActivity extends FragmentActivity { + private static final String TAG = "FlutterActivity"; + + // Meta-data arguments, processed from manifest XML. + private static final String DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint"; + private static final String INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute"; + + // Intent extra arguments. + public static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint"; + public static final String EXTRA_INITIAL_ROUTE = "initial_route"; + + // 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 + private FlutterFragment flutterFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(createFragmentContainer()); + ensureFlutterFragmentCreated(); + } + + /** + * 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() { + return FlutterFragment.newInstance( + getDartEntrypoint(), + getInitialRoute(), + getAppBundlePath(), + FlutterShellArgs.fromIntent(getIntent()) + ); + } + + /** + * 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 + 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()); + } + + /** + * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. + *

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

    + *
  1. Pass a {@code String} as {@link #EXTRA_DART_ENTRYPOINT} with the launching {@code Intent}, or
  2. + *
  3. Set a {@code } called {@link #DART_ENTRYPOINT_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 Dart entrypoint. + */ + @Nullable + protected String getDartEntrypoint() { + if (getIntent().hasExtra(EXTRA_DART_ENTRYPOINT)) { + return getIntent().getStringExtra(EXTRA_DART_ENTRYPOINT); + } + + try { + ActivityInfo activityInfo = getPackageManager().getActivityInfo( + getComponentName(), + PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES + ); + Bundle metadata = activityInfo.metaData; + return metadata != null ? metadata.getString(DART_ENTRYPOINT_META_DATA_KEY) : null; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * 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 #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. + *
+ * 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. + */ + @Nullable + 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; + return metadata != null ? metadata.getString(INITIAL_ROUTE_META_DATA_KEY) : null; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * 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/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterFragment.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterFragment.java new file mode 100644 index 0000000000..89f25e4dd5 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/android/FlutterFragment.java @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.android; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +import io.flutter.embedding.engine.FlutterShellArgs; + +/** + * {@code Fragment} which displays a Flutter UI that takes up all available {@code Fragment} space. + *

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

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

    + *
  1. {@link Activity#onPostResume()}
  2. + *
  3. {@link Activity#onBackPressed()}
  4. + *
  5. {@link Activity#onRequestPermissionsResult(int, String[], int[])} ()}
  6. + *
  7. {@link Activity#onNewIntent(Intent)} ()}
  8. + *
  9. {@link Activity#onUserLeaveHint()}
  10. + *
  11. {@link Activity#onTrimMemory(int)}
  12. + *
+ * Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be sure + * to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than + * {@link Activity#startActivityForResult(Intent, int)}. If the {@code Activity} version of the + * method is invoked then this {@code Fragment} will never receive its + * {@link Fragment#onActivityResult(int, int, Intent)} callback. + *

+ * If convenient, consider using a {@link FlutterActivity} instead of a {@code FlutterFragment} to + * avoid the work of forwarding calls. + *

+ * If Flutter is needed in a location that can only use a {@code View}, consider using a + * {@link FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an + * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a + * {@code Fragment}. + */ +public class FlutterFragment extends Fragment { + private static final String TAG = "FlutterFragment"; + + private static final String ARG_DART_ENTRYPOINT = "dart_entrypoint"; + private static final String ARG_INITIAL_ROUTE = "initial_route"; + private static final String ARG_APP_BUNDLE_PATH = "app_bundle_path"; + private static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args"; + + /** + * Factory method that creates a new {@link FlutterFragment} with a default configuration. + *

+ * @return new {@link FlutterFragment} + */ + public static FlutterFragment newInstance() { + return newInstance( + null, + null, + null, + null + ); + } + + /** + * Factory method that creates a new {@link FlutterFragment} with the given configuration. + *

+ * @param dartEntrypoint the name of the initial Dart method to invoke, defaults to "main" + * @param initialRoute the first route that a Flutter app will render in this {@link FlutterFragment}, + * defaults to "/" + * @param appBundlePath the path to the app bundle which contains the Dart app to execute, defaults + * to {@link FlutterMain#findAppBundlePath(Context)} + * @param flutterShellArgs any special configuration arguments for the Flutter engine + * + * @return a new {@link FlutterFragment} + */ + public static FlutterFragment newInstance(@Nullable String dartEntrypoint, + @Nullable String initialRoute, + @Nullable String appBundlePath, + @Nullable FlutterShellArgs flutterShellArgs) { + FlutterFragment frag = new FlutterFragment(); + + Bundle args = createArgsBundle( + dartEntrypoint, + initialRoute, + appBundlePath, + flutterShellArgs + ); + frag.setArguments(args); + + return frag; + } + + /** + * Creates a {@link Bundle} of arguments that can be used to configure a {@link FlutterFragment}. + * This method is exposed so that developers can create subclasses of {@link FlutterFragment}. + * Subclasses should declare static factories that use this method to create arguments that will + * be understood by the base class, and then the subclass can add any additional arguments it + * wants to this {@link Bundle}. Example: + *

{@code
+   * public static MyFlutterFragment newInstance(String myNewArg) {
+   *   // Create an instance of our subclass Fragment.
+   *   MyFlutterFragment myFrag = new MyFlutterFragment();
+   *
+   *   // Create the Bundle or args that FlutterFragment understands.
+   *   Bundle args = FlutterFragment.createArgsBundle(...);
+   *
+   *   // Add our new args to the bundle.
+   *   args.putString(ARG_MY_NEW_ARG, myNewArg);
+   *
+   *   // Give the args to our subclass Fragment.
+   *   myFrag.setArguments(args);
+   *
+   *   // Return the newly created subclass Fragment.
+   *   return myFrag;
+   * }
+   * }
+ * + * @param dartEntrypoint the name of the initial Dart method to invoke, defaults to "main" + * @param initialRoute the first route that a Flutter app will render in this {@link FlutterFragment}, defaults to "/" + * @param appBundlePath the path to the app bundle which contains the Dart app to execute + * @param flutterShellArgs any special configuration arguments for the Flutter engine + * + * @return Bundle of arguments that configure a {@link FlutterFragment} + */ + protected static Bundle createArgsBundle(@Nullable String dartEntrypoint, + @Nullable String initialRoute, + @Nullable String appBundlePath, + @Nullable FlutterShellArgs flutterShellArgs) { + Bundle args = new Bundle(); + args.putString(ARG_INITIAL_ROUTE, initialRoute); + args.putString(ARG_APP_BUNDLE_PATH, appBundlePath); + args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint); + // TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of conflating. + if (null != flutterShellArgs) { + args.putStringArray(ARG_FLUTTER_INITIALIZATION_ARGS, flutterShellArgs.toArray()); + } + return args; + } + + public FlutterFragment() { + // Ensure that we at least have an empty Bundle of arguments so that we don't + // need to continually check for null arguments before grabbing one. + setArguments(new Bundle()); + } +}