Android Embedding PR37: Separated FlutterActivity and FlutterFragment via FlutterActivityAndFragmentDelegate (flutter/engine#9895)

This commit is contained in:
Matt Carroll 2019-07-29 17:40:25 -07:00 committed by GitHub
parent 1527ec7b6f
commit e1e57a2d94
17 changed files with 1973 additions and 708 deletions

2
DEPS
View File

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

View File

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

View File

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

View File

@ -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.
* <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>
* <strong>Dart entrypoint, initial route, and app bundle path</strong>
* <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".
@ -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".
* <p>
* 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:
* <ul>
* <li>{@link IntentBuilder#dartEntrypoint}</li>
* <li>{@link IntentBuilder#initialRoute}</li>
* </ul>
* <p>
* The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of
* {@code FlutterActivity} by overriding their respective methods:
* <ul>
* <li>{@link #getAppBundlePath()}</li>
* <li>{@link #getDartEntrypoint()}</li>
* <li>{@link #getDartEntrypointFunctionName()}</li>
* <li>{@link #getInitialRoute()}</li>
* </ul>
* If Flutter is needed in a location that cannot use an {@code Activity}, consider using
@ -65,6 +70,19 @@ import io.flutter.view.FlutterMain;
* {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a
* {@code Fragment}.
* <p>
* <strong>FlutterActivity responsibilities</strong>
* <p>
* {@code FlutterActivity} maintains the following responsibilities:
* <ul>
* <li>Displays an Android launch screen.</li>
* <li>Displays a Flutter splash screen.</li>
* <li>Configures the status bar appearance.</li>
* <li>Chooses the Dart execution app bundle path and entrypoint.</li>
* <li>Chooses Flutter's initial route.</li>
* <li>Renders {@code Activity} transparently, if desired.</li>
* <li>Offers hooks for subclasses to provide and configure a {@link FlutterEngine}.</li>
* </ul>
* <p>
* <strong>Launch Screen and Splash Screen</strong>
* <p>
* {@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}.
* <p>
* 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.:
* <p>
* {@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}.
* <p>
* 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}.
* <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() {
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}.
* <p>
* For an explanation of why this control exists, see {@link FlutterFragment.Builder#shouldAttachEngineToActivity()}.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
* <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.
* {@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.
* <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
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.
* <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;
}
/**
* {@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}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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}.
* <p>
* Defaults to {@code true}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
*/

View File

@ -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}.
* <p>
* <strong>Why does this class exist?</strong>
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* <strong>Caution when modifying this class</strong>
* <p>
* 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.
* <p>
* 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. <strong>Do not use this class as a convenient shortcut for any other
* behavior.</strong>
*/
/* 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}.
* <p>
* No further method invocations may occur on this {@code FlutterActivityAndFragmentDelegate}
* after invoking this method. If a method is invoked, an exception will occur.
* <p>
* 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)}.
* <p>
* This method does the following:
* <p>
* <ol>
* <li>Initializes the Flutter system.</li>
* <li>Obtains or creates a {@link FlutterEngine}.</li>
* <li>Creates and configures a {@link PlatformPlugin}.</li>
* <li>Attaches the {@link FlutterEngine} to the surrounding {@code Activity}, if desired.</li>
* <li>Configures the {@link FlutterEngine} via
* {@link Host#configureFlutterEngine(FlutterEngine)}.</li>
* </ol>
*/
void onAttach(@NonNull Context context) {
ensureAlive();
initializeFlutter(context);
// When "retain instance" is true, the FlutterEngine will survive configuration
// changes. Therefore, we create a new one only if one does not already exist.
if (flutterEngine == null) {
setupFlutterEngine();
}
// Regardless of whether or not a FlutterEngine already existed, the PlatformPlugin
// is bound to a specific Activity. Therefore, it needs to be created and configured
// every time this Fragment attaches to a new Activity.
// TODO(mattcarroll): the PlatformPlugin needs to be reimagined because it implicitly takes
// control of the entire window. This is unacceptable for non-fullscreen
// use-cases.
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
if (host.shouldAttachEngineToActivity()) {
// Notify any plugins that are currently attached to our FlutterEngine that they
// are now attached to an Activity.
//
// Passing this Fragment's Lifecycle should be sufficient because as long as this Fragment
// is attached to its Activity, the lifecycles should be in sync. Once this Fragment is
// detached from its Activity, that Activity will be detached from the FlutterEngine, too,
// which means there shouldn't be any possibility for the Fragment Lifecycle to get out of
// sync with the Activity. We use the Fragment's Lifecycle because it is possible that the
// attached Activity is not a LifecycleOwner.
Log.d(TAG, "Attaching FlutterEngine to the Activity that owns this Fragment.");
flutterEngine.getActivityControlSurface().attachToActivity(
host.getActivity(),
host.getLifecycle()
);
}
host.configureFlutterEngine(flutterEngine);
}
private void initializeFlutter(@NonNull Context context) {
FlutterMain.ensureInitializationComplete(
context.getApplicationContext(),
host.getFlutterShellArgs().toArray()
);
}
/**
* Obtains a reference to a FlutterEngine to back this delegate and its {@code host}.
* <p>
* First, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via
* {@link Host#provideFlutterEngine(Context)}.
* <p>
* 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)}.
* <p>
* {@code inflater} and {@code container} may be null when invoked from an {@code Activity}.
* <p>
* 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()}.
* <p>
* This method:
* <p>
* <ol>
* <li>Attaches the {@link FlutterEngine} owned by this delegate to the {@link FlutterView}
* owned by this delegate.</li>
* <li>Begins executing Dart code, if it is not already executing.</li>
* </ol>
*/
void onStart() {
Log.v(TAG, "onStart()");
ensureAlive();
// We post() the code that attaches the FlutterEngine to our FlutterView because there is
// some kind of blocking logic on the native side when the surface is connected. That lag
// causes launching Activitys to wait a second or two before launching. By post()'ing this
// behavior we are able to move this blocking logic to after the Activity's launch.
// TODO(mattcarroll): figure out how to avoid blocking the MAIN thread when connecting a surface
new Handler().post(new Runnable() {
@Override
public void run() {
Log.v(TAG, "Attaching FlutterEngine to FlutterView.");
flutterView.attachToFlutterEngine(flutterEngine);
doInitialFlutterViewRun();
}
});
}
/**
* Starts running Dart within the FlutterView for the first time.
* <p>
* Reloading/restarting Dart within a given FlutterView is not supported. If this method is
* invoked while Dart is already executing then it does nothing.
* <p>
* {@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()}.
* <p>
* 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()}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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()}.
* <p>
* 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()}.
* <p>
* This method:
* <p>
* <ol>
* <li>This method notifies the running Flutter app that it is "paused" as per the Flutter app
* lifecycle.</li>
* <li>Detaches this delegate's {@link FlutterEngine} from this delegate's {@link FlutterView}.</li>
* </ol>
*/
void onStop() {
Log.v(TAG, "onStop()");
ensureAlive();
flutterEngine.getLifecycleChannel().appIsPaused();
flutterView.detachFromFlutterEngine();
}
/**
* Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDestroyView()}.
* <p>
* 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()}.
* <p>
* This method:
* <p>
* <ol>
* <li>Detaches this delegate's {@link FlutterEngine} from its surrounding {@code Activity},
* if it was previously attached.</li>
* <li>Destroys this delegate's {@link PlatformPlugin}.</li>
* <li>Destroys this delegate's {@link FlutterEngine} if
* {@link Host#retainFlutterEngineAfterHostDestruction()} returns false.</li>
* </ol>
*/
void onDetach() {
Log.v(TAG, "onDetach()");
ensureAlive();
if (host.shouldAttachEngineToActivity()) {
// Notify plugins that they are no longer attached to an Activity.
Log.d(TAG, "Detaching FlutterEngine from the Activity that owns this Fragment.");
if (host.getActivity().isChangingConfigurations()) {
flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges();
} else {
flutterEngine.getActivityControlSurface().detachFromActivity();
}
}
// Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment,
// and this Fragment's Activity.
if (platformPlugin != null) {
platformPlugin.destroy();
platformPlugin = null;
}
// Destroy our FlutterEngine if we're not set to retain it.
if (!host.retainFlutterEngineAfterHostDestruction() && !isFlutterEngineFromHost) {
flutterEngine.destroy();
flutterEngine = null;
}
}
/**
* Invoke this from {@link Activity#onBackPressed()}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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[])}.
* <p>
* 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)}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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)}.
* <p>
* 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()}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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)}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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()}.
* <p>
* A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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();
}
}

View File

@ -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.
* <p>
* This interface may be applied to a {@link FragmentActivity} that owns a {@code FlutterFragment}.
*/
public interface FlutterEngineConfigurator {
/**
* Configures the given {@link FlutterEngine}.
* <p>
* 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)}.
* <p>
* 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);
}

View File

@ -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}.
* <p>
* {@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}.
* <p>
* 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);
}

View File

@ -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.
* <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 android.app.Activity#onPostResume()}</li>
* <li>{@link android.app.Activity#onBackPressed()}</li>
* <li>{@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])} ()}</li>
* <li>{@link android.app.Activity#onNewIntent(Intent)} ()}</li>
* <li>{@link android.app.Activity#onUserLeaveHint()}</li>
* <li>{@link android.app.Activity#onTrimMemory(int)}</li>
* <li>{@link #onPostResume()}</li>
* <li>{@link #onBackPressed()}</li>
* <li>{@link #onRequestPermissionsResult(int, String[], int[])} ()}</li>
* <li>{@link #onNewIntent(Intent)} ()}</li>
* <li>{@link #onUserLeaveHint()}</li>
* <li>{@link #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
@ -61,26 +52,51 @@ import io.flutter.view.FlutterMain;
* {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a
* {@code Fragment}.
*/
public class FlutterFragment extends Fragment {
public class FlutterFragment extends Fragment implements FlutterActivityAndFragmentDelegate.Host {
private static final String TAG = "FlutterFragment";
/**
* The Dart entrypoint method name that is executed upon initialization.
*/
protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint";
/**
* Initial Flutter route that is rendered in a Navigator widget.
*/
protected static final String ARG_INITIAL_ROUTE = "initial_route";
/**
* Path to Flutter's Dart code.
*/
protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path";
/**
* Flutter shell arguments.
*/
protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args";
/**
* {@link FlutterView.RenderMode} to be used for the {@link FlutterView} in this
* {@code FlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode";
/**
* {@link FlutterView.TransparencyMode} to be used for the {@link FlutterView} in this
* {@code FlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode";
/**
* See {@link #shouldAttachEngineToActivity()}.
*/
protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity";
@NonNull
public static FlutterFragment createDefaultFlutterFragment() {
return new FlutterFragment.Builder().build();
}
/**
* Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond
* to the values set on this {@code Builder}.
* <p>
* 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()}.
* <p>
* 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}.
* <p>
* First, {@code FlutterFragment} subclasses are given an opportunity to provide a
* {@link FlutterEngine} by overriding {@link #createFlutterEngine(Context)}.
* <p>
* Second, the {@link FragmentActivity} that owns this {@code FlutterFragment} is
* given the opportunity to provide a {@link FlutterEngine} as a {@link FlutterEngineProvider}.
* <p>
* 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.
* <p>
* This method takes precedence for creation of a {@link FlutterEngine} over any owning
* {@code Activity} that may implement {@link FlutterEngineProvider}.
* <p>
* Consider returning a cached {@link FlutterEngine} instance from this method to avoid the
* typical warm-up time that a new {@link FlutterEngine} instance requires.
* <p>
* 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.
* <p>
* 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)}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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.
*
* <p>
* See {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])}
*
* <p>
* @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}.
*
* <p>
* See {@link android.app.Activity#onNewIntent(Intent)}
*
* <p>
* @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.
* <p>
* 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)}.
*
* <p>
* @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.
*
* <p>
* 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.
*
* <p>
* This implementation forwards a memory pressure warning to the running Flutter app.
*
* <p>
* @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.
*
* <p>
* 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.
* <p>
* Defaults to "main".
* <p>
* 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.
* <p>
* Defaults to {@link FlutterMain#findAppBundlePath(Context)}.
* <p>
* 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.
* <p>
* Defaults to {@code null}, which signifies a route of "/" in Flutter.
* <p>
* 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}.
* <p>
* Defaults to {@link FlutterView.RenderMode#surface}.
* <p>
* 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}.
* <p>
* Defaults to {@link FlutterView.TransparencyMode#transparent}.
* <p>
* 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.
* <p>
* 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.
* <p>
* Consider returning a cached {@link FlutterEngine} instance from this method to avoid the
* typical warm-up time that a new {@link FlutterEngine} instance requires.
* <p>
* If null is returned then a new default {@link FlutterEngine} will be created to back this
* {@code FlutterFragment}.
* <p>
* 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.
* <p>
* 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)}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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()}.
* <p>
* 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.
* <p>
* Defaults to false. This method can be overridden in subclasses to retain the
* {@link FlutterEngine}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* Subclasses that override this method must call through to the {@code super} method.
* <p>
* Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
protected void onFirstFrameRendered() {}
/**
* Provides a {@link FlutterEngine} instance to be used by a {@code FlutterFragment}.
* <p>
* {@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}.
* <p>
* 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.
* <p>
* See {@link #setupFlutterEngine()} for more information.
*/
public interface FlutterEngineProvider {
/**
* Returns the {@link FlutterEngine} that should be used by a child {@code FlutterFragment}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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)}.
* <p>
* 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();
}
}

View File

@ -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();
}

View File

@ -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;
}
}
/**

View File

@ -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}.
* <p>
* 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();
}
}

View File

@ -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");
}

View File

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

View File

@ -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=<new_version_hash> -tag "last_updated:<timestamp>"
$ cipd set-tag --version=<new_version_hash> -tag "robolectric_version:<robolectric_version>"
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=last_updated:<timestamp>
Example of a last-updated timestamp: 2019-07-29T15:27:42-0700
You can generate the same date format with `date +%Y-%m-%dT%T%z`.
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=robolectric_version:<robolectric_version>
You can run `cipd describe flutter/android/robolectric_bundle
--version=<new_version_hash>` to verify. You should see:
@ -79,8 +84,8 @@ Tags:
robolectric_version:<robolectric_version>
```
Then update the `DEPS` file to use the new version by pointing to your new
`last_updated_at` tag.
Then update the `DEPS` file (located at /src/flutter/DEPS) to use the new version by pointing to
your new `last_updated_at` tag.
```
'src/third_party/robolectric': {

View File

@ -6,6 +6,7 @@ package io.flutter;
import io.flutter.SmokeTest;
import io.flutter.util.PreconditionsTest;
import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@ -14,7 +15,8 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({
PreconditionsTest.class,
SmokeTest.class
SmokeTest.class,
FlutterActivityAndFragmentDelegateTest.class,
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
public class FlutterTestSuite {}

View File

@ -0,0 +1,502 @@
package io.flutter.embedding.android;
import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.view.FlutterMain;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Config(manifest=Config.NONE)
@RunWith(RobolectricTestRunner.class)
public class FlutterActivityAndFragmentDelegateTest {
private FlutterEngine mockFlutterEngine;
private FakeHost fakeHost;
private FakeHost spyHost;
@Before
public void setup() {
// FlutterMain is utilized statically, therefore we need to inform it to behave differently
// for testing purposes.
FlutterMain.setIsRunningInRobolectricTest(true);
// Create a mocked FlutterEngine for the various interactions required by the delegate
// being tested.
mockFlutterEngine = mockFlutterEngine();
// Create a fake Host, which is required by the delegate being tested.
fakeHost = new FakeHost();
fakeHost.flutterEngine = mockFlutterEngine;
// Create a spy around the FakeHost so that we can verify method invocations.
spyHost = spy(fakeHost);
}
@After
public void teardown() {
// Return FlutterMain to normal.
FlutterMain.setIsRunningInRobolectricTest(false);
}
@Test
public void itSendsLifecycleEventsToFlutter() {
// ---- Test setup ----
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost);
// We're testing lifecycle behaviors, which require/expect that certain methods have already
// been executed by the time they run. Therefore, we run those expected methods first.
delegate.onAttach(RuntimeEnvironment.application);
delegate.onCreateView(null, null, null);
// --- Execute the behavior under test ---
// By the time an Activity/Fragment is started, we don't expect any lifecycle messages
// to have been sent to Flutter.
delegate.onStart();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive();
// When the Activity/Fragment is resumed, a resumed message should have been sent to Flutter.
delegate.onResume();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused();
// When the Activity/Fragment is paused, an inactive message should have been sent to Flutter.
delegate.onPause();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive();
verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused();
// When the Activity/Fragment is stopped, a paused message should have been sent to Flutter.
// Notice that Flutter uses the term "paused" in a different way, and at a different time
// than the Android OS.
delegate.onStop();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive();
verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused();
}
@Test
public void itDefersToTheHostToProvideFlutterEngine() {
// ---- Test setup ----
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is created in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the host was asked to provide a FlutterEngine.
verify(spyHost, times(1)).provideFlutterEngine(any(Context.class));
// Verify that the delegate's FlutterEngine is our mock FlutterEngine.
assertEquals("The delegate failed to use the host's FlutterEngine.", mockFlutterEngine, delegate.getFlutterEngine());
}
@Test
public void itGivesHostAnOpportunityToConfigureFlutterEngine() {
// ---- Test setup ----
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is created in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the host was asked to configure our FlutterEngine.
verify(spyHost, times(1)).configureFlutterEngine(mockFlutterEngine);
}
@Test
public void itSendsInitialRouteToFlutter() {
// ---- Test setup ----
// Set initial route on our fake Host.
spyHost.initialRoute = "/my/route";
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The initial route is sent in onStart().
delegate.onAttach(RuntimeEnvironment.application);
delegate.onCreateView(null, null, null);
delegate.onStart();
// Verify that the navigation channel was given our initial route.
verify(mockFlutterEngine.getNavigationChannel(), times(1)).setInitialRoute("/my/route");
}
@Test
public void itExecutesDartEntrypointProvidedByHost() {
// ---- Test setup ----
// Set Dart entrypoint parameters on fake host.
spyHost.appBundlePath = "/my/bundle/path";
spyHost.dartEntrypointFunctionName = "myEntrypoint";
// Create the DartEntrypoint that we expect to be executed.
DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint(
RuntimeEnvironment.application.getAssets(),
"/my/bundle/path",
"myEntrypoint"
);
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// Dart is executed in onStart().
delegate.onAttach(RuntimeEnvironment.application);
delegate.onCreateView(null, null, null);
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
verify(mockFlutterEngine.getDartExecutor(), times(1)).executeDartEntrypoint(eq(dartEntrypoint));
}
// "Attaching" to the surrounding Activity refers to Flutter being able to control
// system chrome and other Activity-level details. If Flutter is not attached to the
// surrounding Activity, it cannot control those details. This includes plugins.
@Test
public void itAttachesFlutterToTheActivityIfDesired() {
// ---- Test setup ----
// Declare that the host wants Flutter to attach to the surrounding Activity.
spyHost.shouldAttachToActivity = true;
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// Flutter is attached to the surrounding Activity in onAttach.
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the ActivityControlSurface was told to attach to an Activity.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).attachToActivity(any(Activity.class), any(Lifecycle.class));
// Flutter is detached from the surrounding Activity in onDetach.
delegate.onDetach();
// Verify that the ActivityControlSurface was told to detach from the Activity.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).detachFromActivity();
}
// "Attaching" to the surrounding Activity refers to Flutter being able to control
// system chrome and other Activity-level details. If Flutter is not attached to the
// surrounding Activity, it cannot control those details. This includes plugins.
@Test
public void itDoesNotAttachFlutterToTheActivityIfNotDesired() {
// ---- Test setup ----
// Declare that the host does NOT want Flutter to attach to the surrounding Activity.
spyHost.shouldAttachToActivity = false;
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// Flutter is attached to the surrounding Activity in onAttach.
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the ActivityControlSurface was NOT told to attach to an Activity.
verify(mockFlutterEngine.getActivityControlSurface(), never()).attachToActivity(any(Activity.class), any(Lifecycle.class));
// Flutter is detached from the surrounding Activity in onDetach.
delegate.onDetach();
// Verify that the ActivityControlSurface was NOT told to detach from the Activity.
verify(mockFlutterEngine.getActivityControlSurface(), never()).detachFromActivity();
}
@Test
public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and inform our delegate that the back button was pressed.
delegate.onBackPressed();
// Verify that the navigation channel tried to send a message to Flutter.
verify(mockFlutterEngine.getNavigationChannel(), times(1)).popRoute();
}
@Test
public void itForwardsOnRequestPermissionsResultToFlutterEngine() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onRequestPermissionsResult(0, new String[]{}, new int[]{});
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onRequestPermissionsResult(any(Integer.class), any(String[].class), any(int[].class));
}
@Test
public void itForwardsOnNewIntentToFlutterEngine() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onNewIntent(mock(Intent.class));
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onNewIntent(any(Intent.class));
}
@Test
public void itForwardsOnActivityResultToFlutterEngine() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onActivityResult(0, 0, null);
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onActivityResult(any(Integer.class), any(Integer.class), any(Intent.class));
}
@Test
public void itForwardsOnUserLeaveHintToFlutterEngine() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onUserLeaveHint();
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getActivityControlSurface(), times(1)).onUserLeaveHint();
}
@Test
public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onTrimMemory(TRIM_MEMORY_RUNNING_LOW);
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning();
}
@Test
public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Emulate the host and call the method that we expect to be forwarded.
delegate.onLowMemory();
// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning();
}
/**
* Creates a mock {@link FlutterEngine}.
* <p>
* 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.
* <p>
* 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() {}
}
}

View File

@ -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();