blob: d22118d4391b147a975b46c3a78cd39595f55485 [file] [log] [blame]
// 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 static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import io.flutter.FlutterInjector;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterEngineGroup;
import io.flutter.embedding.engine.FlutterEngineGroupCache;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.Arrays;
import java.util.List;
/**
* 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 */ class FlutterActivityAndFragmentDelegate implements ExclusiveAppComponent<Activity> {
private static final String TAG = "FlutterActivityAndFragmentDelegate";
private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework";
private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins";
private static final int FLUTTER_SPLASH_VIEW_FALLBACK_ID = 486947586;
/** Factory to obtain a FlutterActivityAndFragmentDelegate instance. */
public interface DelegateFactory {
FlutterActivityAndFragmentDelegate createDelegate(FlutterActivityAndFragmentDelegate.Host host);
}
// The FlutterActivity or FlutterFragment that is delegating most of its calls
// to this FlutterActivityAndFragmentDelegate.
@NonNull private Host host;
@Nullable private FlutterEngine flutterEngine;
@VisibleForTesting @Nullable FlutterView flutterView;
@Nullable private PlatformPlugin platformPlugin;
@VisibleForTesting @Nullable OnPreDrawListener activePreDrawListener;
private boolean isFlutterEngineFromHost;
private boolean isFlutterUiDisplayed;
private boolean isFirstFrameRendered;
private boolean isAttached;
private Integer previousVisibility;
@Nullable private FlutterEngineGroup engineGroup;
@NonNull
private final FlutterUiDisplayListener flutterUiDisplayListener =
new FlutterUiDisplayListener() {
@Override
public void onFlutterUiDisplayed() {
host.onFlutterUiDisplayed();
isFlutterUiDisplayed = true;
isFirstFrameRendered = true;
}
@Override
public void onFlutterUiNoLongerDisplayed() {
host.onFlutterUiNoLongerDisplayed();
isFlutterUiDisplayed = false;
}
};
FlutterActivityAndFragmentDelegate(@NonNull Host host) {
this(host, null);
}
FlutterActivityAndFragmentDelegate(@NonNull Host host, @Nullable FlutterEngineGroup engineGroup) {
this.host = host;
this.isFirstFrameRendered = false;
this.engineGroup = engineGroup;
}
/**
* 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
* io.flutter.embedding.engine.FlutterEngine}. The behavior that destroys a {@link
* io.flutter.embedding.engine.FlutterEngine} can be found in {@link #onDetach()}.
*/
void release() {
this.host = null;
this.flutterEngine = null;
this.flutterView = null;
this.platformPlugin = null;
}
/**
* Returns the {@link io.flutter.embedding.engine.FlutterEngine} that is owned by this delegate
* and its host {@code Activity} or {@code Fragment}.
*/
@Nullable
/* package */ FlutterEngine getFlutterEngine() {
return flutterEngine;
}
/**
* Returns true if the host {@code Activity}/{@code Fragment} provided a {@code FlutterEngine}, as
* opposed to this delegate creating a new one.
*/
/* package */ boolean isFlutterEngineFromHost() {
return isFlutterEngineFromHost;
}
/**
* Whether or not this {@code FlutterActivityAndFragmentDelegate} is attached to a {@code
* FlutterEngine}.
*/
/* package */ boolean isAttached() {
return isAttached;
}
/**
* 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>Obtains or creates a {@link io.flutter.embedding.engine.FlutterEngine}.
* <li>Creates and configures a {@link PlatformPlugin}.
* <li>Attaches the {@link io.flutter.embedding.engine.FlutterEngine} to the surrounding {@code
* Activity}, if desired.
* <li>Configures the {@link io.flutter.embedding.engine.FlutterEngine} via {@link
* Host#configureFlutterEngine(FlutterEngine)}.
* </ol>
*/
void onAttach(@NonNull Context context) {
ensureAlive();
// 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();
}
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.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
// 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);
host.configureFlutterEngine(flutterEngine);
isAttached = true;
}
@Override
public @NonNull Activity getAppComponent() {
final Activity activity = host.getActivity();
if (activity == null) {
throw new AssertionError(
"FlutterActivityAndFragmentDelegate's getAppComponent should only "
+ "be queried after onAttach, when the host's activity should always be non-null");
}
return activity;
}
private FlutterEngineGroup.Options addEntrypointOptions(FlutterEngineGroup.Options options) {
String appBundlePathOverride = host.getAppBundlePath();
if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath();
}
DartExecutor.DartEntrypoint dartEntrypoint =
new DartExecutor.DartEntrypoint(
appBundlePathOverride, host.getDartEntrypointFunctionName());
return options
.setDartEntrypoint(dartEntrypoint)
.setInitialRoute(host.getInitialRoute())
.setDartEntrypointArgs(host.getDartEntrypointArgs());
}
/**
* Obtains a reference to a FlutterEngine to back this delegate and its {@code host}.
*
* <p>
*
* <p>First, the {@code host} is asked if it would like to use a cached {@link
* io.flutter.embedding.engine.FlutterEngine}, and if so, the cached {@link
* io.flutter.embedding.engine.FlutterEngine} is retrieved.
*
* <p>Second, the {@code host} is given an opportunity to provide a {@link
* io.flutter.embedding.engine.FlutterEngine} via {@link Host#provideFlutterEngine(Context)}.
*
* <p>Third, the {@code host} is asked if it would like to use a cached {@link
* io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link FlutterEngine} by {@link
* FlutterEngineGroup#createAndRunEngine}
*
* <p>If the {@code host} does not provide a {@link io.flutter.embedding.engine.FlutterEngine},
* then a new {@link FlutterEngine} is instantiated.
*/
@VisibleForTesting
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");
// First, check if the host wants to use a cached FlutterEngine.
String cachedEngineId = host.getCachedEngineId();
if (cachedEngineId != null) {
flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId);
isFlutterEngineFromHost = true;
if (flutterEngine == null) {
throw new IllegalStateException(
"The requested cached FlutterEngine did not exist in the FlutterEngineCache: '"
+ cachedEngineId
+ "'");
}
return;
}
// Second, defer to subclasses for a custom FlutterEngine.
flutterEngine = host.provideFlutterEngine(host.getContext());
if (flutterEngine != null) {
isFlutterEngineFromHost = true;
return;
}
// Third, check if the host wants to use a cached FlutterEngineGroup
// and create new FlutterEngine using FlutterEngineGroup#createAndRunEngine
String cachedEngineGroupId = host.getCachedEngineGroupId();
if (cachedEngineGroupId != null) {
FlutterEngineGroup flutterEngineGroup =
FlutterEngineGroupCache.getInstance().get(cachedEngineGroupId);
if (flutterEngineGroup == null) {
throw new IllegalStateException(
"The requested cached FlutterEngineGroup did not exist in the FlutterEngineGroupCache: '"
+ cachedEngineGroupId
+ "'");
}
flutterEngine =
flutterEngineGroup.createAndRunEngine(
addEntrypointOptions(new FlutterEngineGroup.Options(host.getContext())));
isFlutterEngineFromHost = false;
return;
}
// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");
FlutterEngineGroup group =
engineGroup == null
? new FlutterEngineGroup(host.getContext(), host.getFlutterShellArgs().toArray())
: engineGroup;
flutterEngine =
group.createAndRunEngine(
addEntrypointOptions(
new FlutterEngineGroup.Options(host.getContext())
.setAutomaticallyRegisterPlugins(false)
.setWaitForRestorationData(host.shouldRestoreAndSaveState())));
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>{@code shouldDelayFirstAndroidViewDraw} determines whether to set up an {@link
* android.view.ViewTreeObserver.OnPreDrawListener}, which will defer the current drawing pass
* till after the Flutter UI has been displayed. This results in more accurate timings reported
* with Android tools, such as "Displayed" timing printed with `am start`.
*
* <p>Note that it should only be set to true when {@code Host#getRenderMode()} is {@code
* RenderMode.surface}. This parameter is also ignored, disabling the delay should the legacy
* {@code Host#provideSplashScreen()} be non-null. See <a
* href="https://flutter.dev/go/android-splash-migration">Android Splash Migration</a>.
*
* <p>This method:
*
* <ol>
* <li>creates a new {@link FlutterView} in a {@code View} hierarchy
* <li>adds a {@link FlutterUiDisplayListener} to it
* <li>attaches a {@link io.flutter.embedding.engine.FlutterEngine} to the new {@link
* FlutterView}
* <li>returns the new {@code View} hierarchy
* </ol>
*/
@NonNull
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw) {
Log.v(TAG, "Creating FlutterView.");
ensureAlive();
if (host.getRenderMode() == RenderMode.surface) {
FlutterSurfaceView flutterSurfaceView =
new FlutterSurfaceView(
host.getContext(), host.getTransparencyMode() == TransparencyMode.transparent);
// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterSurfaceViewCreated(flutterSurfaceView);
// Create the FlutterView that owns the FlutterSurfaceView.
flutterView = new FlutterView(host.getContext(), flutterSurfaceView);
} else {
FlutterTextureView flutterTextureView = new FlutterTextureView(host.getContext());
flutterTextureView.setOpaque(host.getTransparencyMode() == TransparencyMode.opaque);
// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterTextureViewCreated(flutterTextureView);
// Create the FlutterView that owns the FlutterTextureView.
flutterView = new FlutterView(host.getContext(), flutterTextureView);
}
// Add listener to be notified when Flutter renders its first frame.
flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
Log.v(TAG, "Attaching FlutterEngine to FlutterView.");
flutterView.attachToFlutterEngine(flutterEngine);
flutterView.setId(flutterViewId);
SplashScreen splashScreen = host.provideSplashScreen();
if (splashScreen != null) {
Log.w(
TAG,
"A splash screen was provided to Flutter, but this is deprecated. See"
+ " flutter.dev/go/android-splash-migration for migration steps.");
FlutterSplashView flutterSplashView = new FlutterSplashView(host.getContext());
flutterSplashView.setId(ViewUtils.generateViewId(FLUTTER_SPLASH_VIEW_FALLBACK_ID));
flutterSplashView.displayFlutterViewWithSplash(flutterView, splashScreen);
return flutterSplashView;
}
if (shouldDelayFirstAndroidViewDraw) {
delayFirstAndroidViewDraw(flutterView);
}
return flutterView;
}
void onRestoreInstanceState(@Nullable Bundle bundle) {
Log.v(
TAG,
"onRestoreInstanceState. Giving framework and plugins an opportunity to restore state.");
ensureAlive();
Bundle pluginState = null;
byte[] frameworkState = null;
if (bundle != null) {
pluginState = bundle.getBundle(PLUGINS_RESTORATION_BUNDLE_KEY);
frameworkState = bundle.getByteArray(FRAMEWORK_RESTORATION_BUNDLE_KEY);
}
if (host.shouldRestoreAndSaveState()) {
flutterEngine.getRestorationChannel().setRestorationData(frameworkState);
}
if (host.shouldAttachEngineToActivity()) {
flutterEngine.getActivityControlSurface().onRestoreInstanceState(pluginState);
}
}
/**
* Invoke this from {@code Activity#onStart()} or {@code Fragment#onStart()}.
*
* <p>This method:
*
* <p>
*
* <ol>
* <li>Begins executing Dart code, if it is not already executing.
* </ol>
*/
void onStart() {
Log.v(TAG, "onStart()");
ensureAlive();
doInitialFlutterViewRun();
// This is a workaround for a bug on some OnePlus phones. The visibility of the application
// window is still true after locking the screen on some OnePlus phones, and shows a black
// screen when unlocked. We can work around this by changing the visibility of FlutterView in
// onStart and onStop.
// See https://github.com/flutter/flutter/issues/93276
if (previousVisibility != null) {
flutterView.setVisibility(previousVisibility);
}
}
/**
* 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() {
// Don't attempt to start a FlutterEngine if we're using a cached FlutterEngine.
if (host.getCachedEngineId() != null) {
return;
}
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;
}
String initialRoute = host.getInitialRoute();
if (initialRoute == null) {
initialRoute = maybeGetInitialRouteFromIntent(host.getActivity().getIntent());
if (initialRoute == null) {
initialRoute = DEFAULT_INITIAL_ROUTE;
}
}
@Nullable String libraryUri = host.getDartEntrypointLibraryUri();
Log.v(
TAG,
"Executing Dart entrypoint: "
+ host.getDartEntrypointFunctionName()
+ ", library uri: "
+ libraryUri
== null
? "\"\""
: libraryUri + ", and sending initial route: " + initialRoute);
// 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.
flutterEngine.getNavigationChannel().setInitialRoute(initialRoute);
String appBundlePathOverride = host.getAppBundlePath();
if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath();
}
// Configure the Dart entrypoint and execute it.
DartExecutor.DartEntrypoint entrypoint =
libraryUri == null
? new DartExecutor.DartEntrypoint(
appBundlePathOverride, host.getDartEntrypointFunctionName())
: new DartExecutor.DartEntrypoint(
appBundlePathOverride, libraryUri, host.getDartEntrypointFunctionName());
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint, host.getDartEntrypointArgs());
}
private String maybeGetInitialRouteFromIntent(Intent intent) {
if (host.shouldHandleDeeplinking()) {
Uri data = intent.getData();
if (data != null) {
String fullRoute = data.getPath();
if (fullRoute != null && !fullRoute.isEmpty()) {
if (data.getQuery() != null && !data.getQuery().isEmpty()) {
fullRoute += "?" + data.getQuery();
}
if (data.getFragment() != null && !data.getFragment().isEmpty()) {
fullRoute += "#" + data.getFragment();
}
return fullRoute;
}
}
}
return null;
}
/**
* Delays the first drawing of the {@code flutterView} until the Flutter first has been displayed.
*/
private void delayFirstAndroidViewDraw(FlutterView flutterView) {
if (host.getRenderMode() != RenderMode.surface) {
// Using a TextureView will cause a deadlock, where the underlying SurfaceTexture is never
// available since it will wait for drawing to be completed first. At the same time, the
// preDraw listener keeps returning false since the Flutter Engine waits for the
// SurfaceTexture to be available.
throw new IllegalArgumentException(
"Cannot delay the first Android view draw when the render mode is not set to"
+ " `RenderMode.surface`.");
}
if (activePreDrawListener != null) {
flutterView.getViewTreeObserver().removeOnPreDrawListener(activePreDrawListener);
}
activePreDrawListener =
new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (isFlutterUiDisplayed && activePreDrawListener != null) {
flutterView.getViewTreeObserver().removeOnPreDrawListener(this);
activePreDrawListener = null;
}
return isFlutterUiDisplayed;
}
};
flutterView.getViewTreeObserver().addOnPreDrawListener(activePreDrawListener);
}
/**
* 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();
if (host.shouldDispatchAppLifecycleState()) {
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) {
updateSystemUiOverlays();
} else {
Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity.");
}
}
/**
* Refreshes Android's window system UI (AKA system chrome) to match Flutter's desired system
* chrome style.
*/
void updateSystemUiOverlays() {
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();
}
}
/**
* 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();
if (host.shouldDispatchAppLifecycleState()) {
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>Detaches this delegate's {@link io.flutter.embedding.engine.FlutterEngine} from this
* delegate's {@link FlutterView}.
* </ol>
*/
void onStop() {
Log.v(TAG, "onStop()");
ensureAlive();
if (host.shouldDispatchAppLifecycleState()) {
flutterEngine.getLifecycleChannel().appIsPaused();
}
// This is a workaround for a bug on some OnePlus phones. The visibility of the application
// window is still true after locking the screen on some OnePlus phones, and shows a black
// screen when unlocked. We can work around this by changing the visibility of FlutterView in
// onStart and onStop.
// See https://github.com/flutter/flutter/issues/93276
previousVisibility = flutterView.getVisibility();
flutterView.setVisibility(View.GONE);
}
/**
* Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDestroyView()}.
*
* <p>This method removes this delegate's {@link FlutterView}'s {@link FlutterUiDisplayListener}.
*/
void onDestroyView() {
Log.v(TAG, "onDestroyView()");
ensureAlive();
if (activePreDrawListener != null) {
flutterView.getViewTreeObserver().removeOnPreDrawListener(activePreDrawListener);
activePreDrawListener = null;
}
flutterView.detachFromFlutterEngine();
flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener);
}
void onSaveInstanceState(@Nullable Bundle bundle) {
Log.v(TAG, "onSaveInstanceState. Giving framework and plugins an opportunity to save state.");
ensureAlive();
if (host.shouldRestoreAndSaveState()) {
bundle.putByteArray(
FRAMEWORK_RESTORATION_BUNDLE_KEY,
flutterEngine.getRestorationChannel().getRestorationData());
}
if (host.shouldAttachEngineToActivity()) {
final Bundle plugins = new Bundle();
flutterEngine.getActivityControlSurface().onSaveInstanceState(plugins);
bundle.putBundle(PLUGINS_RESTORATION_BUNDLE_KEY, plugins);
}
}
@Override
public void detachFromFlutterEngine() {
if (host.shouldDestroyEngineWithHost()) {
// The host owns the engine and should never have its engine taken by another exclusive
// activity.
throw new AssertionError(
"The internal FlutterEngine created by "
+ host
+ " has been attached to by another activity. To persist a FlutterEngine beyond the "
+ "ownership of this activity, explicitly create a FlutterEngine");
}
// Default, but customizable, behavior is for the host to call {@link #onDetach}
// deterministically as to not mix more events during the lifecycle of the next exclusive
// activity.
host.detachFromFlutterEngine();
}
/**
* Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDetach()}.
*
* <p>This method:
*
* <p>
*
* <ol>
* <li>Detaches this delegate's {@link io.flutter.embedding.engine.FlutterEngine} from its
* surrounding {@code Activity}, if it was previously attached.
* <li>Destroys this delegate's {@link PlatformPlugin}.
* <li>Destroys this delegate's {@link io.flutter.embedding.engine.FlutterEngine} if {@link
* Host#shouldDestroyEngineWithHost()} ()} returns true.
* </ol>
*/
void onDetach() {
Log.v(TAG, "onDetach()");
ensureAlive();
// Give the host an opportunity to cleanup any references that were created in
// configureFlutterEngine().
host.cleanUpFlutterEngine(flutterEngine);
if (host.shouldAttachEngineToActivity()) {
// Notify plugins that they are no longer attached to an Activity.
Log.v(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;
}
if (host.shouldDispatchAppLifecycleState()) {
flutterEngine.getLifecycleChannel().appIsDetached();
}
// Destroy our FlutterEngine if we're not set to retain it.
if (host.shouldDestroyEngineWithHost()) {
flutterEngine.destroy();
if (host.getCachedEngineId() != null) {
FlutterEngineCache.getInstance().remove(host.getCachedEngineId());
}
flutterEngine = null;
}
isAttached = false;
}
/**
* Invoke this from {@link android.app.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 android.app.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 and sending pushRouteInformation message.");
flutterEngine.getActivityControlSurface().onNewIntent(intent);
String initialRoute = maybeGetInitialRouteFromIntent(intent);
if (initialRoute != null && !initialRoute.isEmpty()) {
flutterEngine.getNavigationChannel().pushRouteInformation(initialRoute);
}
} 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 android.app.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.
// Avoid being too aggressive before the first frame is rendered. If it is
// not at least running critical, we should avoid delaying the frame for
// an overly aggressive GC.
boolean trim = isFirstFrameRendered && level >= TRIM_MEMORY_RUNNING_LOW;
if (trim) {
flutterEngine.getDartExecutor().notifyLowMemoryWarning();
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
flutterEngine.getRenderer().onTrimMemory(level);
}
}
/**
* 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,
PlatformPlugin.PlatformPluginDelegate {
/**
* Returns the {@link Context} that backs the host {@link android.app.Activity} or {@code
* Fragment}.
*/
@NonNull
Context getContext();
/** Returns true if the delegate should retrieve the initial route from the {@link Intent}. */
@Nullable
boolean shouldHandleDeeplinking();
/**
* Returns the host {@link android.app.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 android.app.Activity} or {@code
* Fragment}.
*/
@NonNull
Lifecycle getLifecycle();
/** Returns the {@link FlutterShellArgs} that should be used when initializing Flutter. */
@NonNull
FlutterShellArgs getFlutterShellArgs();
/**
* Returns the ID of a statically cached {@link io.flutter.embedding.engine.FlutterEngine} to
* use within this delegate's host, or {@code null} if this delegate's host does not want to use
* a cached {@link FlutterEngine}.
*/
@Nullable
String getCachedEngineId();
@Nullable
String getCachedEngineGroupId();
/**
* Returns true if the {@link io.flutter.embedding.engine.FlutterEngine} used in this delegate
* should be destroyed when the host/delegate are destroyed.
*
* <p>The default value is {@code true} in cases where {@code FlutterFragment} created its own
* {@link io.flutter.embedding.engine.FlutterEngine}, and {@code false} in cases where a cached
* {@link io.flutter.embedding.engine.FlutterEngine} was provided.
*/
boolean shouldDestroyEngineWithHost();
/**
* Callback called when the {@link io.flutter.embedding.engine.FlutterEngine} has been attached
* to by another activity before this activity was destroyed.
*
* <p>The expected behavior is for this activity to synchronously stop using the {@link
* FlutterEngine} to avoid lifecycle crosstalk with the new activity.
*/
void detachFromFlutterEngine();
/**
* Returns the Dart entrypoint that should run when a new {@link
* io.flutter.embedding.engine.FlutterEngine} is created.
*/
@NonNull
String getDartEntrypointFunctionName();
/**
* Returns the URI of the Dart library which contains the entrypoint method (example
* "package:foo_package/main.dart"). If null, this will default to the same library as the
* `main()` function in the Dart program.
*/
@Nullable
String getDartEntrypointLibraryUri();
/** Returns arguments that passed as a list of string to Dart's entrypoint function. */
@Nullable
List<String> getDartEntrypointArgs();
/** 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 RenderMode} used by the {@link FlutterView} that displays the {@link
* FlutterEngine}'s content.
*/
@NonNull
RenderMode getRenderMode();
/**
* Returns the {@link TransparencyMode} used by the {@link FlutterView} that displays the {@link
* FlutterEngine}'s content.
*/
@NonNull
TransparencyMode getTransparencyMode();
/**
* Returns the {@link ExclusiveAppComponent<Activity>} that is associated with {@link
* io.flutter.embedding.engine.FlutterEngine}.
*
* <p>In the scenario where multiple {@link FlutterActivity} or {@link FlutterFragment} share
* the same {@link FlutterEngine}, to attach/re-attache a {@link FlutterActivity} or {@link
* FlutterFragment} to the shared {@link FlutterEngine}, we MUST manually invoke {@link
* ActivityControlSurface#attachToActivity(ExclusiveAppComponent, Lifecycle)}.
*
* <p>The {@link ExclusiveAppComponent} is exposed here so that subclasses of {@link
* FlutterActivity} or {@link FlutterFragment} can access it.
*/
ExclusiveAppComponent<Activity> getExclusiveAppComponent();
@Nullable
SplashScreen provideSplashScreen();
/**
* Returns the {@link io.flutter.embedding.engine.FlutterEngine} that should be rendered to a
* {@link FlutterView}.
*
* <p>If {@code null} is returned, a new {@link io.flutter.embedding.engine.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 io.flutter.embedding.engine.FlutterEngine} as
* desired.
*/
void configureFlutterEngine(@NonNull FlutterEngine flutterEngine);
/**
* Hook for the host to cleanup references that were established in {@link
* #configureFlutterEngine(FlutterEngine)} before the host is destroyed or detached.
*/
void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine);
/**
* Returns true if the {@link io.flutter.embedding.engine.FlutterEngine}'s plugin system should
* be connected to the host {@link android.app.Activity}, allowing plugins to interact with it.
*/
boolean shouldAttachEngineToActivity();
/**
* Invoked by this delegate when the {@link FlutterSurfaceView} that renders the Flutter UI is
* initially instantiated.
*
* <p>This method is only invoked if the {@link
* io.flutter.embedding.android.FlutterView.RenderMode} is set to {@link
* io.flutter.embedding.android.FlutterView.RenderMode#surface}. Otherwise, {@link
* #onFlutterTextureViewCreated(FlutterTextureView)} is invoked.
*
* <p>This method is invoked before the given {@link FlutterSurfaceView} is attached to the
* {@code View} hierarchy. Implementers should not attempt to climb the {@code View} hierarchy
* or make assumptions about relationships with other {@code View}s.
*/
void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView);
/**
* Invoked by this delegate when the {@link FlutterTextureView} that renders the Flutter UI is
* initially instantiated.
*
* <p>This method is only invoked if the {@link
* io.flutter.embedding.android.FlutterView.RenderMode} is set to {@link
* io.flutter.embedding.android.FlutterView.RenderMode#texture}. Otherwise, {@link
* #onFlutterSurfaceViewCreated(FlutterSurfaceView)} is invoked.
*
* <p>This method is invoked before the given {@link FlutterTextureView} is attached to the
* {@code View} hierarchy. Implementers should not attempt to climb the {@code View} hierarchy
* or make assumptions about relationships with other {@code View}s.
*/
void onFlutterTextureViewCreated(@NonNull FlutterTextureView flutterTextureView);
/** Invoked by this delegate when its {@link FlutterView} starts painting pixels. */
void onFlutterUiDisplayed();
/** Invoked by this delegate when its {@link FlutterView} stops painting pixels. */
void onFlutterUiNoLongerDisplayed();
/**
* Whether state restoration is enabled.
*
* <p>When this returns true, the instance state provided to {@code
* onRestoreInstanceState(Bundle)} will be forwarded to the framework via the {@code
* RestorationChannel} and during {@code onSaveInstanceState(Bundle)} the current framework
* instance state obtained from {@code RestorationChannel} will be stored in the provided
* bundle.
*
* <p>This defaults to true, unless a cached engine is used.
*/
boolean shouldRestoreAndSaveState();
/**
* Refreshes Android's window system UI (AKA system chrome) to match Flutter's desired system
* chrome style.
*
* <p>This is useful when using the splash screen API available in Android 12. {@code
* SplashScreenView#remove} resets the system UI colors to the values set prior to the execution
* of the Dart entrypoint. As a result, the values set from Dart are reverted by this API. To
* workaround this issue, call this method after removing the splash screen with {@code
* SplashScreenView#remove}.
*/
void updateSystemUiOverlays();
/**
* Give the host application a chance to take control of the app lifecycle events to avoid
* lifecycle crosstalk.
*
* <p>In the add-to-app scenario where multiple {@link FlutterActivity} shares the same {@link
* FlutterEngine}, the application lifecycle state will have crosstalk causing the page to
* freeze. For example, we open a new page called FlutterActivity#2 from the previous page
* called FlutterActivity#1. The flow of app lifecycle states received by dart is as follows:
*
* <p>inactive (from FlutterActivity#1) -> resumed (from FlutterActivity#2) -> paused (from
* FlutterActivity#1)
*
* <p>On the one hand, the {@code paused} state from FlutterActivity#1 will cause the
* FlutterActivity#2 page to be stuck; On the other hand, these states are not expected from the
* perspective of the entire application lifecycle. If the host application gets the control of
* sending {@link AppLifecycleState}, It will be possible to correctly match the {@link
* AppLifecycleState} with the application-level lifecycle.
*
* <p>Return {@code false} means the host application dispatches these app lifecycle events,
* while return {@code true} means the engine dispatches these events.
*/
boolean shouldDispatchAppLifecycleState();
}
}