diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index e71e364881..8b77bc7378 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -20,7 +20,6 @@ import androidx.annotation.VisibleForTesting; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.view.TextureRegistry; -import io.flutter.view.TextureRegistry.ImageTextureEntry; import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; @@ -343,6 +342,7 @@ public class FlutterRenderer implements TextureRegistry { private static final String TAG = "ImageTextureRegistryEntry"; private final long id; private boolean released; + private boolean ignoringFence = false; private Image image; ImageTextureRegistryEntry(long id) { @@ -390,27 +390,45 @@ public class FlutterRenderer implements TextureRegistry { } } - @Override @TargetApi(33) + private void waitOnFence(Image image) { + try { + SyncFence fence = image.getFence(); + boolean signaled = fence.awaitForever(); + if (!signaled) { + Log.e(TAG, "acquireLatestImage image's fence was never signalled."); + } + } catch (IOException e) { + // Drop. + } + } + + @TargetApi(29) + private void maybeWaitOnFence(Image image) { + if (image == null) { + return; + } + if (Build.VERSION.SDK_INT >= 33) { + // The fence API is only available on Android >= 33. + waitOnFence(image); + return; + } + if (!ignoringFence) { + // Log once per ImageTextureEntry. + ignoringFence = true; + Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33"); + } + } + + @Override + @TargetApi(29) public Image acquireLatestImage() { Image r; synchronized (this) { r = this.image; this.image = null; } - if (r != null) { - try { - SyncFence fence = r.getFence(); - if (fence.getSignalTime() == SyncFence.SIGNAL_TIME_PENDING) { - boolean signaled = fence.awaitForever(); - if (!signaled) { - Log.e(TAG, "acquireLatestImage image's fence was never signalled."); - } - } - } catch (IOException e) { - // Drop. - } - } + maybeWaitOnFence(r); return r; } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTarget.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTarget.java index 79690179a8..29443aeb63 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTarget.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTarget.java @@ -12,7 +12,7 @@ import android.view.Surface; import io.flutter.Log; import io.flutter.view.TextureRegistry.ImageTextureEntry; -@TargetApi(33) +@TargetApi(29) public class ImageReaderPlatformViewRenderTarget implements PlatformViewRenderTarget { private ImageTextureEntry textureEntry; private ImageReader reader; @@ -72,18 +72,33 @@ public class ImageReaderPlatformViewRenderTarget implements PlatformViewRenderTa return reader; } + @TargetApi(29) + protected ImageReader createImageReader29() { + final ImageReader reader = + ImageReader.newInstance( + bufferWidth, + bufferHeight, + ImageFormat.PRIVATE, + MAX_IMAGES, + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); + reader.setOnImageAvailableListener(this.onImageAvailableListener, onImageAvailableHandler); + return reader; + } + protected ImageReader createImageReader() { if (Build.VERSION.SDK_INT >= 33) { return createImageReader33(); + } else if (Build.VERSION.SDK_INT >= 29) { + return createImageReader29(); } throw new UnsupportedOperationException( - "ImageReaderPlatformViewRenderTarget requires API version 33+"); + "ImageReaderPlatformViewRenderTarget requires API version 29+"); } public ImageReaderPlatformViewRenderTarget(ImageTextureEntry textureEntry) { - if (Build.VERSION.SDK_INT < 33) { + if (Build.VERSION.SDK_INT < 29) { throw new UnsupportedOperationException( - "ImageReaderPlatformViewRenderTarget requires API version 33+"); + "ImageReaderPlatformViewRenderTarget requires API version 29+"); } this.textureEntry = textureEntry; } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTargetTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTargetTest.java new file mode 100644 index 0000000000..64a0fd1e13 --- /dev/null +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/ImageReaderPlatformViewRenderTargetTest.java @@ -0,0 +1,105 @@ +// 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.plugin.platform; + +import static android.os.Looper.getMainLooper; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.robolectric.Shadows.shadowOf; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.media.Image; +import android.view.View; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.view.TextureRegistry.ImageTextureEntry; +import org.junit.Test; +import org.junit.runner.RunWith; + +@TargetApi(29) +@RunWith(AndroidJUnit4.class) +public class ImageReaderPlatformViewRenderTargetTest { + private final Context ctx = ApplicationProvider.getApplicationContext(); + + class TestImageTextureEntry implements ImageTextureEntry { + private Image lastPushedImage; + + public long id() { + return 1; + } + + public void release() { + if (this.lastPushedImage != null) { + this.lastPushedImage.close(); + } + } + + public void pushImage(Image image) { + if (this.lastPushedImage != null) { + this.lastPushedImage.close(); + } + this.lastPushedImage = image; + } + + public Image acquireLatestImage() { + Image r = this.lastPushedImage; + this.lastPushedImage = null; + return r; + } + } + + @Test + public void viewDraw_writesToBuffer() { + final TestImageTextureEntry textureEntry = new TestImageTextureEntry(); + final ImageReaderPlatformViewRenderTarget renderTarget = + new ImageReaderPlatformViewRenderTarget(textureEntry); + // Custom view. + final View platformView = + new View(ctx) { + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + canvas.drawColor(Color.RED); + } + }; + final int size = 100; + platformView.measure(size, size); + platformView.layout(0, 0, size, size); + renderTarget.resize(size, size); + + // We don't have an image in the texture entry. + assertNull(textureEntry.acquireLatestImage()); + + // Start rendering a frame. + final Canvas targetCanvas = renderTarget.lockHardwareCanvas(); + assertNotNull(targetCanvas); + + try { + // Fill the render target with transparent pixels. This is needed for platform views that + // expect a transparent background. + targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Override the canvas that this subtree of views will use to draw. + platformView.draw(targetCanvas); + } finally { + // Finish rendering a frame. + renderTarget.unlockCanvasAndPost(targetCanvas); + } + + // Pump the UI thread task loop. This is needed so that the OnImageAvailable callback + // gets invoked (resulting in textureEntry.pushImage being invoked). + shadowOf(getMainLooper()).idle(); + + // An image was pushed into the texture entry and it has the correct dimensions. + Image pushedImage = textureEntry.acquireLatestImage(); + assertNotNull(pushedImage); + assertEquals(pushedImage.getWidth(), size); + assertEquals(pushedImage.getHeight(), size); + } +} diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index fe8c7e4696..3a77e6ae68 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -1,3 +1,7 @@ +// 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.plugin.platform; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java index ffb50fb65b..29ad40daae 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java @@ -1,3 +1,7 @@ +// 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.plugin.platform; import static android.view.View.OnFocusChangeListener; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 13ec6823f1..3bb1dc7ba7 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -1,3 +1,7 @@ +// 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.plugin.platform; import static android.os.Looper.getMainLooper; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java index 20e6bb3b47..0466827976 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java @@ -1,3 +1,7 @@ +// 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.plugin.platform; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java index a1ab6d75e1..020ef090f3 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java @@ -1,3 +1,7 @@ +// 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.plugin.platform; import static org.junit.Assert.*;