Android PR 7: Introduce structure of FlutterActivity and FlutterFragment (flutter/engine#7912)

This commit is contained in:
Matt Carroll
2019-02-26 01:48:09 -08:00
committed by GitHub
parent 43e43f846b
commit e58bcfa1aa
5 changed files with 529 additions and 0 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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.
* <p>
* WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE.
* IF YOU USE IT, WE WILL BREAK YOU.
* <p>
* 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<String> 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<String> 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<String>}.
* The given arguments are automatically de-duplicated.
*/
public FlutterShellArgs(@NonNull List<String> args) {
this.args = new HashSet<>(args);
}
/**
* Creates a set of Flutter shell arguments from a given {@code Set<String>}.
*/
public FlutterShellArgs(@NonNull Set<String> 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);
}
}

View File

@@ -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.
* <p>
* WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE.
* IF YOU USE IT, WE WILL BREAK YOU.
* <p>
* {@code FlutterActivity} is the simplest and most direct way to integrate Flutter within an
* Android app.
* <p>
* 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".
* <p>
* 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".
* <p>
* The app bundle path, Dart entrypoint, and initial route can each be controlled in a subclass of
* {@code FlutterActivity} by overriding their respective methods:
* <ul>
* <li>{@link #getAppBundlePath()}</li>
* <li>{@link #getDartEntrypoint()}</li>
* <li>{@link #getInitialRoute()}</li>
* </ul>
* 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}.
* <p>
* 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}.
* <p>
* @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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* By default, the app bundle path is obtained from {@link FlutterMain#findAppBundlePath(Context)}.
* <p>
* 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.
* <p>
* This preference can be controlled with 2 methods:
* <ol>
* <li>Pass a {@code String} as {@link #EXTRA_DART_ENTRYPOINT} with the launching {@code Intent}, or</li>
* <li>Set a {@code <meta-data>} called {@link #DART_ENTRYPOINT_META_DATA_KEY} for this
* {@code Activity} in the Android manifest.</li>
* </ol>
* If both preferences are set, the {@code Intent} preference takes priority.
* <p>
* The reason that a {@code <meta-data>} 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}.
* <p>
* 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.
* <p>
* This preference can be controlled with 2 methods:
* <ol>
* <li>Pass a boolean as {@link #EXTRA_INITIAL_ROUTE} with the launching {@code Intent}, or</li>
* <li>Set a {@code <meta-data>} called {@link #INITIAL_ROUTE_META_DATA_KEY} for this
* {@code Activity} in the Android manifest.</li>
* </ol>
* If both preferences are set, the {@code Intent} preference takes priority.
* <p>
* The reason that a {@code <meta-data>} 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}.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@@ -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.
* <p>
* WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE.
* IF YOU USE IT, WE WILL BREAK YOU.
* <p>
* Using a {@code FlutterFragment} requires forwarding a number of calls from an {@code Activity} to
* ensure that the internal Flutter app behaves as expected:
* <ol>
* <li>{@link Activity#onPostResume()}</li>
* <li>{@link Activity#onBackPressed()}</li>
* <li>{@link Activity#onRequestPermissionsResult(int, String[], int[])} ()}</li>
* <li>{@link Activity#onNewIntent(Intent)} ()}</li>
* <li>{@link Activity#onUserLeaveHint()}</li>
* <li>{@link Activity#onTrimMemory(int)}</li>
* </ol>
* 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.
* <p>
* If convenient, consider using a {@link FlutterActivity} instead of a {@code FlutterFragment} to
* avoid the work of forwarding calls.
* <p>
* 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.
* <ul>
* <li>default Dart entrypoint of "main"</li>
* <li>initial route of "/"</li>
* <li>default app bundle location</li>
* <li>no special engine arguments</li>
* </ul>
* @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.
* <p>
* @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:
* <pre>{@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;
* }
* }</pre>
*
* @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());
}
}