diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 8cf69c24cf568..1cb273e32a009 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -949,13 +949,22 @@ public void detachFromFlutterEngine() { flutterEngine = null; } + @VisibleForTesting + @NonNull + public FlutterImageView createImageView() { + return new FlutterImageView( + getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + } + + /** + * Converts the current render surface to a {@link FlutterImageView} if it's not one already. + * Otherwise, it resizes the {@link FlutterImageView} based on the current view size. + */ public void convertToImageView() { renderSurface.pause(); if (flutterImageView == null) { - flutterImageView = - new FlutterImageView( - getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + flutterImageView = createImageView(); addView(flutterImageView); } else { flutterImageView.resizeIfNeeded(getWidth(), getHeight()); @@ -969,10 +978,13 @@ public void convertToImageView() { } /** - * If the surface is rendered by a {@code FlutterImageView}. Then, calling this method will stop - * rendering to a {@code FlutterImageView}, and use the previous surface instead. + * If the surface is rendered by a {@link FlutterImageView}, then calling this method will stop + * rendering to a {@link FlutterImageView}, and render on the previous surface instead. + * + * @param onDone a callback called when Flutter UI is rendered on the previous surface. Use this + * callback to perform cleanups. For example, destroy overlay surfaces. */ - public void revertImageView() { + public void revertImageView(@NonNull Runnable onDone) { if (flutterImageView == null) { Log.v(TAG, "Tried to revert the image view, but no image view is used."); return; @@ -981,12 +993,39 @@ public void revertImageView() { Log.v(TAG, "Tried to revert the image view, but no previous surface was used."); return; } - flutterImageView.detachFromRenderer(); renderSurface = previousRenderSurface; previousRenderSurface = null; - if (flutterEngine != null) { - renderSurface.attachToRenderer(flutterEngine.getRenderer()); + if (flutterEngine == null) { + flutterImageView.detachFromRenderer(); + onDone.run(); + return; + } + final FlutterRenderer renderer = flutterEngine.getRenderer(); + if (renderer == null) { + flutterImageView.detachFromRenderer(); + onDone.run(); + return; } + // Start rendering on the previous surface. + // This surface is typically `FlutterSurfaceView` or `FlutterTextureView`. + renderSurface.attachToRenderer(renderer); + + // Install a Flutter UI listener to wait until the first frame is rendered + // in the new surface to call the `onDone` callback. + renderer.addIsDisplayingFlutterUiListener( + new FlutterUiDisplayListener() { + @Override + public void onFlutterUiDisplayed() { + renderer.removeIsDisplayingFlutterUiListener(this); + onDone.run(); + flutterImageView.detachFromRenderer(); + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + // no-op + } + }); } public void attachOverlaySurfaceToRender(FlutterImageView view) { diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 1291d5db05c96..59fa7132e3008 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -90,7 +90,7 @@ * *
To invoke a native method that is not associated with a platform view, invoke it statically: * - *
{@code bool enabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled(); } + *
{@code bool enabled = FlutterJNI.getIsSoftwareRenderingEnabled(); }
*/
@Keep
public class FlutterJNI {
@@ -120,9 +120,13 @@ public static native void nativeInit(
*/
public static native void nativePrefetchDefaultFontManager();
- // TODO(mattcarroll): add javadocs
+ private native boolean nativeGetIsSoftwareRenderingEnabled();
+
@UiThread
- public native boolean nativeGetIsSoftwareRenderingEnabled();
+ // TODO(mattcarroll): add javadocs
+ public boolean getIsSoftwareRenderingEnabled() {
+ return nativeGetIsSoftwareRenderingEnabled();
+ }
@Nullable
// TODO(mattcarroll): add javadocs
@@ -212,7 +216,12 @@ public boolean isAttached() {
public void attachToNative(boolean isBackgroundView) {
ensureRunningOnMainThread();
ensureNotAttachedToNative();
- nativePlatformViewId = nativeAttach(this, isBackgroundView);
+ nativePlatformViewId = performNativeAttach(this, isBackgroundView);
+ }
+
+ @VisibleForTesting
+ public long performNativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgroundView) {
+ return nativeAttach(flutterJNI, isBackgroundView);
}
private native long nativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgroundView);
@@ -279,7 +288,7 @@ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListene
@SuppressWarnings("unused")
@VisibleForTesting
@UiThread
- void onFirstFrame() {
+ public void onFirstFrame() {
ensureRunningOnMainThread();
for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
index f0cd24adea675..0aa11c4f2aacf 100644
--- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
+++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
@@ -314,7 +314,7 @@ private void unregisterTexture(long textureId) {
// TODO(mattcarroll): describe the native behavior that this invokes
public boolean isSoftwareRenderingEnabled() {
- return flutterJNI.nativeGetIsSoftwareRenderingEnabled();
+ return flutterJNI.getIsSoftwareRenderingEnabled();
}
// TODO(mattcarroll): describe the native behavior that this invokes
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index ee7d393320a2b..7c064a6f90818 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -713,7 +713,7 @@ public void onDisplayPlatformView(
int width,
int height,
int viewWidth,
- int ViewHeight,
+ int viewHeight,
FlutterMutatorsStack mutatorsStack) {
initializeRootImageViewIfNeeded();
initializePlatformViewIfNeeded(viewId);
@@ -723,7 +723,7 @@ public void onDisplayPlatformView(
mutatorView.setVisibility(View.VISIBLE);
mutatorView.bringToFront();
- FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, ViewHeight);
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, viewHeight);
View platformView = platformViews.get(viewId);
platformView.setLayoutParams(layoutParams);
platformView.bringToFront();
@@ -753,6 +753,20 @@ public void onBeginFrame() {
}
public void onEndFrame() {
+ final FlutterView view = (FlutterView) flutterView;
+ // If there are no platform views in the current frame,
+ // then revert the image view surface and use the previous surface.
+ //
+ // Otherwise, acquire the latest image.
+ if (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) {
+ flutterViewConvertedToImageView = false;
+ view.revertImageView(
+ () -> {
+ // Destroy overlay surfaces once the surface reversion is completed.
+ finishFrame(false);
+ });
+ return;
+ }
// Whether the current frame was rendered using ImageReaders.
//
// Since the image readers may not have images available at this point,
@@ -762,22 +776,12 @@ public void onEndFrame() {
// If one of the surfaces doesn't have an image, the frame may be incomplete and must be
// dropped.
// For example, a toolbar widget painted by Flutter may not be rendered.
- boolean isFrameRenderedUsingImageReaders = false;
-
- if (flutterViewConvertedToImageView) {
- FlutterView view = (FlutterView) flutterView;
- // If there are no platform views in the current frame,
- // then revert the image view surface and use the previous surface.
- //
- // Otherwise, acquire the latest image.
- if (currentFrameUsedPlatformViewIds.isEmpty()) {
- view.revertImageView();
- flutterViewConvertedToImageView = false;
- } else {
- isFrameRenderedUsingImageReaders = view.acquireLatestImageViewFrame();
- }
- }
+ boolean isFrameRenderedUsingImageReaders =
+ flutterViewConvertedToImageView && view.acquireLatestImageViewFrame();
+ finishFrame(isFrameRenderedUsingImageReaders);
+ }
+ private void finishFrame(boolean isFrameRenderedUsingImageReaders) {
for (int i = 0; i < overlayLayerViews.size(); i++) {
int overlayId = overlayLayerViews.keyAt(i);
FlutterImageView overlayView = overlayLayerViews.valueAt(i);
@@ -818,6 +822,14 @@ public void onEndFrame() {
}
}
+ @VisibleForTesting
+ @TargetApi(19)
+ public FlutterOverlaySurface createOverlaySurface(@NonNull FlutterImageView imageView) {
+ final int id = nextOverlayLayerId++;
+ overlayLayerViews.put(id, imageView);
+ return new FlutterOverlaySurface(id, imageView.getSurface());
+ }
+
@TargetApi(19)
public FlutterOverlaySurface createOverlaySurface() {
// Overlay surfaces have the same size as the background surface.
@@ -826,17 +838,12 @@ public FlutterOverlaySurface createOverlaySurface() {
// if the drawings they contain have a different tight bound.
//
// The final view size is determined when its frame is set.
- FlutterImageView imageView =
+ return createOverlaySurface(
new FlutterImageView(
flutterView.getContext(),
flutterView.getWidth(),
flutterView.getHeight(),
- FlutterImageView.SurfaceKind.overlay);
-
- int id = nextOverlayLayerId++;
- overlayLayerViews.put(id, imageView);
-
- return new FlutterOverlaySurface(id, imageView.getSurface());
+ FlutterImageView.SurfaceKind.overlay));
}
public void destroyOverlaySurfaces() {
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index ed1499ced9641..f472b07c0753c 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -169,7 +169,7 @@ public FlutterView(Context context, AttributeSet attrs, FlutterNativeView native
dartExecutor = mNativeView.getDartExecutor();
flutterRenderer = new FlutterRenderer(mNativeView.getFlutterJNI());
- mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().nativeGetIsSoftwareRenderingEnabled();
+ mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().getIsSoftwareRenderingEnabled();
mMetrics = new ViewportMetrics();
mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
setFocusable(true);
diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
index fad8dca431591..659056afc5132 100644
--- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
@@ -8,15 +8,29 @@
import android.content.Context;
import android.content.res.AssetManager;
import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
import android.view.View;
import android.view.ViewParent;
+import io.flutter.embedding.android.FlutterImageView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
+import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
+import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorView;
+import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack;
+import io.flutter.embedding.engine.renderer.FlutterRenderer;
+import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
+import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
+import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
+import io.flutter.embedding.engine.systemchannels.SettingsChannel;
+import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.StandardMethodCodec;
+import io.flutter.plugin.localization.LocalizationPlugin;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
@@ -27,6 +41,9 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowSurfaceView;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
@@ -196,6 +213,7 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() {
}
@Test
+ @Config(shadows = {ShadowFlutterJNI.class})
public void getPlatformViewById__hybridComposition() {
PlatformViewsController platformViewsController = new PlatformViewsController();
@@ -223,6 +241,7 @@ public void getPlatformViewById__hybridComposition() {
}
@Test
+ @Config(shadows = {ShadowFlutterJNI.class})
public void initializePlatformViewIfNeeded__throwsIfViewIsNull() {
PlatformViewsController platformViewsController = new PlatformViewsController();
@@ -254,6 +273,7 @@ public void initializePlatformViewIfNeeded__throwsIfViewIsNull() {
}
@Test
+ @Config(shadows = {ShadowFlutterJNI.class})
public void initializePlatformViewIfNeeded__throwsIfViewHasParent() {
PlatformViewsController platformViewsController = new PlatformViewsController();
@@ -286,6 +306,7 @@ public void initializePlatformViewIfNeeded__throwsIfViewHasParent() {
}
@Test
+ @Config(shadows = {ShadowFlutterJNI.class})
public void disposeAndroidView__hybridComposition() {
PlatformViewsController platformViewsController = new PlatformViewsController();
@@ -324,10 +345,72 @@ public void disposeAndroidView__hybridComposition() {
assertTrue(androidView.getParent() instanceof FlutterMutatorView);
}
+ @Test
+ @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class})
+ public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() {
+ final PlatformViewsController platformViewsController = new PlatformViewsController();
+
+ final int platformViewId = 0;
+ assertNull(platformViewsController.getPlatformViewById(platformViewId));
+
+ final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
+ final PlatformView platformView = mock(PlatformView.class);
+ when(platformView.getView()).thenReturn(mock(View.class));
+ when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
+
+ platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);
+
+ final FlutterJNI jni = new FlutterJNI();
+ jni.attachToNative(false);
+ attach(jni, platformViewsController);
+
+ jni.onFirstFrame();
+
+ // Simulate create call from the framework.
+ createPlatformView(jni, platformViewsController, platformViewId, "testType");
+
+ // Produce a frame that displays a platform view and an overlay surface.
+ platformViewsController.onBeginFrame();
+ platformViewsController.onDisplayPlatformView(
+ platformViewId,
+ /* x=*/ 0,
+ /* y=*/ 0,
+ /* width=*/ 10,
+ /* height=*/ 10,
+ /* viewWidth=*/ 10,
+ /* viewHeight=*/ 10,
+ /* mutatorsStack=*/ new FlutterMutatorsStack());
+
+ final FlutterImageView overlayImageView = mock(FlutterImageView.class);
+ when(overlayImageView.acquireLatestImage()).thenReturn(true);
+
+ final FlutterOverlaySurface overlaySurface =
+ platformViewsController.createOverlaySurface(overlayImageView);
+ platformViewsController.onDisplayOverlaySurface(
+ overlaySurface.getId(), /* x=*/ 0, /* y=*/ 0, /* width=*/ 10, /* height=*/ 10);
+
+ platformViewsController.onEndFrame();
+
+ // Simulate first frame from the framework.
+ jni.onFirstFrame();
+
+ verify(overlayImageView, never()).detachFromRenderer();
+
+ // Produce a frame that doesn't display platform views.
+ platformViewsController.onBeginFrame();
+ platformViewsController.onEndFrame();
+
+ verify(overlayImageView, never()).detachFromRenderer();
+
+ // Simulate first frame from the framework.
+ jni.onFirstFrame();
+ verify(overlayImageView, times(1)).detachFromRenderer();
+ }
+
private static byte[] encodeMethodCall(MethodCall call) {
- ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call);
+ final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call);
buffer.rewind();
- byte[] dest = new byte[buffer.remaining()];
+ final byte[] dest = new byte[buffer.remaining()];
buffer.get(dest);
return dest;
}
@@ -337,12 +420,13 @@ private static void createPlatformView(
PlatformViewsController platformViewsController,
int platformViewId,
String viewType) {
- Map