Fix compatibility issues with SurfaceTexture on Android Q. (flutter/engine#31698)

This commit is contained in:
Rulong Chen(陈汝龙) 2022-03-08 02:01:26 +08:00 committed by GitHub
parent 23bef4dc9b
commit 4f6f461dac
7 changed files with 135 additions and 15 deletions

View File

@ -121,10 +121,20 @@ public class FlutterRenderer implements TextureRegistry {
private final long id;
@NonNull private final SurfaceTextureWrapper textureWrapper;
private boolean released;
@Nullable private OnFrameConsumedListener listener;
private final Runnable onFrameConsumed =
new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onFrameConsumed();
}
}
};
SurfaceTextureRegistryEntry(long id, @NonNull SurfaceTexture surfaceTexture) {
this.id = id;
this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture);
this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture, onFrameConsumed);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// The callback relies on being executed on the UI thread (unsynchronised read of
@ -195,6 +205,11 @@ public class FlutterRenderer implements TextureRegistry {
super.finalize();
}
}
@Override
public void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {
this.listener = listener;
}
}
static final class SurfaceTextureFinalizerRunnable implements Runnable {

View File

@ -7,6 +7,7 @@ package io.flutter.embedding.engine.renderer;
import android.graphics.SurfaceTexture;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A wrapper for a SurfaceTexture that tracks whether the texture has been released.
@ -20,10 +21,25 @@ public class SurfaceTextureWrapper {
private SurfaceTexture surfaceTexture;
private boolean released;
private boolean attached;
private Runnable onFrameConsumed;
public SurfaceTextureWrapper(@NonNull SurfaceTexture surfaceTexture) {
this(surfaceTexture, null);
}
/**
* A wrapper for a SurfaceTexture.
*
* <p>The provided {@code onFrameConsumed} callback must be invoked when the most recent image was
* consumed.
*
* @param onFrameConsumed The callback after the {@code updateTexImage} is called.
*/
public SurfaceTextureWrapper(
@NonNull SurfaceTexture surfaceTexture, @Nullable Runnable onFrameConsumed) {
this.surfaceTexture = surfaceTexture;
this.released = false;
this.onFrameConsumed = onFrameConsumed;
}
@NonNull
@ -37,6 +53,9 @@ public class SurfaceTextureWrapper {
synchronized (this) {
if (!released) {
surfaceTexture.updateTexImage();
if (onFrameConsumed != null) {
onFrameConsumed.run();
}
}
}
}

View File

@ -26,6 +26,8 @@ import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.util.ViewUtils;
import io.flutter.view.TextureRegistry;
import java.util.concurrent.atomic.AtomicLong;
/**
* Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}.
@ -52,12 +54,45 @@ class PlatformViewWrapper extends FrameLayout {
private AndroidTouchProcessor touchProcessor;
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
@Nullable private TextureRegistry.SurfaceTextureEntry textureEntry;
private final AtomicLong pendingFramesCount = new AtomicLong(0L);
private final TextureRegistry.OnFrameConsumedListener listener =
new TextureRegistry.OnFrameConsumedListener() {
@Override
public void onFrameConsumed() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.decrementAndGet();
}
}
};
private void onFrameProduced() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.incrementAndGet();
}
}
private boolean shouldDrawToSurfaceNow() {
if (Build.VERSION.SDK_INT == 29) {
return pendingFramesCount.get() <= 0L;
}
return true;
}
public PlatformViewWrapper(@NonNull Context context) {
super(context);
setWillNotDraw(false);
}
public PlatformViewWrapper(
@NonNull Context context, @NonNull TextureRegistry.SurfaceTextureEntry textureEntry) {
this(context);
this.textureEntry = textureEntry;
textureEntry.setOnFrameConsumedListener(listener);
setTexture(textureEntry.surfaceTexture());
}
/**
* Sets the touch processor that allows to intercept gestures.
*
@ -109,6 +144,7 @@ class PlatformViewWrapper extends FrameLayout {
} else {
canvas.drawColor(Color.TRANSPARENT);
}
onFrameProduced();
} finally {
surface.unlockCanvasAndPost(canvas);
}
@ -202,19 +238,29 @@ class PlatformViewWrapper extends FrameLayout {
Log.e(TAG, "Invalid texture. The platform view cannot be displayed.");
return;
}
// Override the canvas that this subtree of views will use to draw.
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
try {
// Clear the current pixels in the canvas.
// This helps when a WebView renders an HTML document with transparent background.
if (Build.VERSION.SDK_INT >= 29) {
surfaceCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
} else {
surfaceCanvas.drawColor(Color.TRANSPARENT);
// We've observed on Android Q that we have to wait for the consumer of {@link SurfaceTexture}
// to consume the last image before continuing to draw, otherwise subsequent calls to
// {@code dequeueBuffer} to request a free buffer from the {@link BufferQueue} will fail.
// See https://github.com/flutter/flutter/issues/98722
if (!shouldDrawToSurfaceNow()) {
// If there are still frames that are not consumed, we will draw them next time.
invalidate();
} else {
// Override the canvas that this subtree of views will use to draw.
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
try {
// Clear the current pixels in the canvas.
// This helps when a WebView renders an HTML document with transparent background.
if (Build.VERSION.SDK_INT >= 29) {
surfaceCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
} else {
surfaceCanvas.drawColor(Color.TRANSPARENT);
}
super.draw(surfaceCanvas);
onFrameProduced();
} finally {
surface.unlockCanvasAndPost(surfaceCanvas);
}
super.draw(surfaceCanvas);
} finally {
surface.unlockCanvasAndPost(surfaceCanvas);
}
}

View File

@ -188,10 +188,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
final PlatformView platformView = viewFactory.create(context, viewId, createParams);
platformViews.put(viewId, platformView);
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context);
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
wrapperView.setTexture(textureEntry.surfaceTexture());
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context, textureEntry);
wrapperView.setTouchProcessor(androidTouchProcessor);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);

View File

@ -917,6 +917,7 @@ public class FlutterView extends SurfaceView
// still be called by a stale reference after released==true and mNativeView==null.
return;
}
mNativeView
.getFlutterJNI()
.markTextureFrameAvailable(SurfaceTextureRegistryEntry.this.id);

View File

@ -6,6 +6,7 @@ package io.flutter.view;
import android.graphics.SurfaceTexture;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
// TODO(mattcarroll): re-evalute docs in this class and add nullability annotations.
/**
@ -41,5 +42,17 @@ public interface TextureRegistry {
/** Deregisters and releases this SurfaceTexture. */
void release();
/** Set a listener that will be notified when the most recent image has been consumed. */
default void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {}
}
/** Listener invoked when the most recent image has been consumed. */
interface OnFrameConsumedListener {
/**
* This method will to be invoked when the most recent image from the image stream has been
* consumed.
*/
void onFrameConsumed();
}
}

View File

@ -18,8 +18,10 @@ import android.os.Looper;
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.view.TextureRegistry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -312,4 +314,29 @@ public class FlutterRendererTest {
},
stateCaptor.getValue());
}
@Test
public void itNotifyImageFrameListener() {
// Setup the test.
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
AtomicInteger invocationCount = new AtomicInteger(0);
final TextureRegistry.OnFrameConsumedListener listener =
new TextureRegistry.OnFrameConsumedListener() {
@Override
public void onFrameConsumed() {
invocationCount.incrementAndGet();
}
};
FlutterRenderer.SurfaceTextureRegistryEntry entry =
(FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture();
entry.setOnFrameConsumedListener(listener);
// Execute the behavior under test.
entry.textureWrapper().updateTexImage();
// Verify behavior under test.
assertEquals(1, invocationCount.get());
}
}