| // 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()); |
| String initialRoute = host.getInitialRoute(); |
| if (initialRoute == null) { |
| initialRoute = maybeGetInitialRouteFromIntent(host.getActivity().getIntent()); |
| if (initialRoute == null) { |
| initialRoute = DEFAULT_INITIAL_ROUTE; |
| } |
| } |
| return options |
| .setDartEntrypoint(dartEntrypoint) |
| .setInitialRoute(initialRoute) |
| .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 != null) { |
| 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 != null) { |
| 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 != null) { |
| 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 can be null in instances where a delegate.onDestroyView is called without |
| // onCreateView being called. See https://github.com/flutter/engine/pull/41082 for more detail. |
| if (flutterView != 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 != null) { |
| 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 {@code Activity#onWindowFocusChanged()}. |
| * |
| * <p>A {@code Fragment} host must have its containing {@code Activity} forward this call so that |
| * the {@code Fragment} can then invoke this method. |
| */ |
| void onWindowFocusChanged(boolean hasFocus) { |
| ensureAlive(); |
| Log.v(TAG, "Received onWindowFocusChanged: " + (hasFocus ? "true" : "false")); |
| if (host.shouldDispatchAppLifecycleState() && flutterEngine != null) { |
| // TODO(gspencergoog): Once we have support for multiple windows/views, |
| // this code will need to consult the list of windows/views to determine if |
| // any windows in the app are focused and call the appropriate function. |
| if (hasFocus) { |
| flutterEngine.getLifecycleChannel().aWindowIsFocused(); |
| } else { |
| flutterEngine.getLifecycleChannel().noWindowsAreFocused(); |
| } |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| } |