mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Android Embedding PR37: Separated FlutterActivity and FlutterFragment via FlutterActivityAndFragmentDelegate (flutter/engine#9895)
This commit is contained in:
parent
1527ec7b6f
commit
e1e57a2d94
2
DEPS
2
DEPS
@ -474,7 +474,7 @@ deps = {
|
||||
'packages': [
|
||||
{
|
||||
'package': 'flutter/android/robolectric_bundle',
|
||||
'version': 'last_updated:2019-07-22@11:16:04-07:00'
|
||||
'version': 'last_updated:2019-07-29T15:27:42-0700'
|
||||
}
|
||||
],
|
||||
'condition': 'download_android_deps',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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() {}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user