blob: df0e5b556a0d3be8282e3707225be9b31977d2fe [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.engine.renderer;
import static io.flutter.Build.API_LEVELS;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.HardwareBuffer;
import android.hardware.SyncFence;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.view.TextureRegistry;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents the rendering responsibilities of a {@code FlutterEngine}.
*
* <p>{@code FlutterRenderer} works in tandem with a provided {@link RenderSurface} to paint Flutter
* pixels to an Android {@code View} hierarchy.
*
* <p>{@code FlutterRenderer} manages textures for rendering, and forwards some Java calls to native
* Flutter code via JNI. The corresponding {@link RenderSurface} provides the Android {@link
* Surface} that this renderer paints.
*
* <p>{@link io.flutter.embedding.android.FlutterSurfaceView} and {@link
* io.flutter.embedding.android.FlutterTextureView} are implementations of {@link RenderSurface}.
*/
public class FlutterRenderer implements TextureRegistry {
/**
* Whether to always use GL textures for {@link FlutterRenderer#createSurfaceProducer()}.
*
* <p>This is a debug-only API intended for local development. For example, when using a newer
* Android device (that normally would use {@link ImageReaderSurfaceProducer}, but wanting to test
* the OpenGLES/{@link SurfaceTextureSurfaceProducer} code branch. This flag has undefined
* behavior if set to true while running in a Vulkan (Impeller) context.
*/
@VisibleForTesting public static boolean debugForceSurfaceProducerGlTextures = false;
private static final String TAG = "FlutterRenderer";
@NonNull private final FlutterJNI flutterJNI;
@NonNull private final AtomicLong nextTextureId = new AtomicLong(0L);
@Nullable private Surface surface;
private boolean isDisplayingFlutterUi = false;
private final Handler handler = new Handler();
@NonNull
private final Set<WeakReference<TextureRegistry.OnTrimMemoryListener>> onTrimMemoryListeners =
new HashSet<>();
@NonNull
private final FlutterUiDisplayListener flutterUiDisplayListener =
new FlutterUiDisplayListener() {
@Override
public void onFlutterUiDisplayed() {
isDisplayingFlutterUi = true;
}
@Override
public void onFlutterUiNoLongerDisplayed() {
isDisplayingFlutterUi = false;
}
};
public FlutterRenderer(@NonNull FlutterJNI flutterJNI) {
this.flutterJNI = flutterJNI;
this.flutterJNI.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
}
/**
* Returns true if this {@code FlutterRenderer} is painting pixels to an Android {@code View}
* hierarchy, false otherwise.
*/
public boolean isDisplayingFlutterUi() {
return isDisplayingFlutterUi;
}
/**
* Adds a listener that is invoked whenever this {@code FlutterRenderer} starts and stops painting
* pixels to an Android {@code View} hierarchy.
*/
public void addIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
flutterJNI.addIsDisplayingFlutterUiListener(listener);
if (isDisplayingFlutterUi) {
listener.onFlutterUiDisplayed();
}
}
/**
* Removes a listener added via {@link
* #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
*/
public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
flutterJNI.removeIsDisplayingFlutterUiListener(listener);
}
private void clearDeadListeners() {
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
while (iterator.hasNext()) {
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
final OnTrimMemoryListener listener = listenerRef.get();
if (listener == null) {
iterator.remove();
}
}
}
/** Adds a listener that is invoked when a memory pressure warning was forward. */
@VisibleForTesting
/* package */ void addOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
// Purge dead listener to avoid accumulating.
clearDeadListeners();
onTrimMemoryListeners.add(new WeakReference<>(listener));
}
/**
* Removes a {@link OnTrimMemoryListener} that was added with {@link
* #addOnTrimMemoryListener(OnTrimMemoryListener)}.
*/
@VisibleForTesting
/* package */ void removeOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
for (WeakReference<OnTrimMemoryListener> listenerRef : onTrimMemoryListeners) {
if (listenerRef.get() == listener) {
onTrimMemoryListeners.remove(listenerRef);
break;
}
}
}
// ------ START TextureRegistry IMPLEMENTATION -----
/**
* Creates and returns a new external texture {@link SurfaceProducer} managed by the Flutter
* engine that is also made available to Flutter code.
*/
@NonNull
@Override
public SurfaceProducer createSurfaceProducer() {
// Prior to Impeller, Flutter on Android *only* ran on OpenGLES (via Skia). That
// meant that
// plugins (i.e. end-users) either explicitly created a SurfaceTexture (via
// createX/registerX) or an ImageTexture (via createX/registerX).
//
// In an Impeller world, which for the first time uses (if available) a Vulkan
// rendering
// backend, it is no longer possible (at least not trivially) to render an
// OpenGLES-provided
// texture (SurfaceTexture) in a Vulkan context.
//
// This function picks the "best" rendering surface based on the Android
// runtime, and
// provides a consumer-agnostic SurfaceProducer (which in turn vends a Surface),
// and has
// plugins (i.e. end-users) use the Surface instead, letting us "hide" the
// consumer-side
// of the implementation.
//
// tl;dr: If ImageTexture is available, we use it, otherwise we use a
// SurfaceTexture.
// Coincidentally, if ImageTexture is available, we are also on an Android
// version that is
// running Vulkan, so we don't have to worry about it not being supported.
final SurfaceProducer entry;
if (!debugForceSurfaceProducerGlTextures && Build.VERSION.SDK_INT >= API_LEVELS.API_29) {
final long id = nextTextureId.getAndIncrement();
final ImageReaderSurfaceProducer producer = new ImageReaderSurfaceProducer(id);
registerImageTexture(id, producer);
addOnTrimMemoryListener(producer);
Log.v(TAG, "New ImageReaderSurfaceProducer ID: " + id);
entry = producer;
} else {
// TODO(matanlurey): Actually have the class named "*Producer" to well, produce
// something. This is a code smell, but does guarantee the paths for both
// createSurfaceTexture and createSurfaceProducer doesn't diverge. As we get more
// confident in this API and any possible bugs (and have tests to check we don't
// regress), reconsider this pattern.
final SurfaceTextureEntry texture = createSurfaceTexture();
final SurfaceTextureSurfaceProducer producer =
new SurfaceTextureSurfaceProducer(texture.id(), handler, flutterJNI, texture);
Log.v(TAG, "New SurfaceTextureSurfaceProducer ID: " + texture.id());
entry = producer;
}
return entry;
}
/**
* Creates and returns a new {@link SurfaceTexture} managed by the Flutter engine that is also
* made available to Flutter code.
*/
@NonNull
@Override
public SurfaceTextureEntry createSurfaceTexture() {
Log.v(TAG, "Creating a SurfaceTexture.");
final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
return registerSurfaceTexture(surfaceTexture);
}
/**
* Registers and returns a {@link SurfaceTexture} managed by the Flutter engine that is also made
* available to Flutter code.
*/
@NonNull
@Override
public SurfaceTextureEntry registerSurfaceTexture(@NonNull SurfaceTexture surfaceTexture) {
return registerSurfaceTexture(nextTextureId.getAndIncrement(), surfaceTexture);
}
/**
* Similar to {@link FlutterRenderer#registerSurfaceTexture} but with an existing @{code
* textureId}.
*
* @param surfaceTexture Surface texture to wrap.
* @param textureId A texture ID already created that should be assigned to the surface texture.
*/
@NonNull
private SurfaceTextureEntry registerSurfaceTexture(
long textureId, @NonNull SurfaceTexture surfaceTexture) {
surfaceTexture.detachFromGLContext();
final SurfaceTextureRegistryEntry entry =
new SurfaceTextureRegistryEntry(textureId, surfaceTexture);
Log.v(TAG, "New SurfaceTexture ID: " + entry.id());
registerTexture(entry.id(), entry.textureWrapper());
addOnTrimMemoryListener(entry);
return entry;
}
@NonNull
@Override
public ImageTextureEntry createImageTexture() {
final ImageTextureRegistryEntry entry =
new ImageTextureRegistryEntry(nextTextureId.getAndIncrement());
Log.v(TAG, "New ImageTextureEntry ID: " + entry.id());
registerImageTexture(entry.id(), entry);
return entry;
}
@Override
public void onTrimMemory(int level) {
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
while (iterator.hasNext()) {
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
final OnTrimMemoryListener listener = listenerRef.get();
if (listener != null) {
listener.onTrimMemory(level);
} else {
// Purge cleared refs to avoid accumulating a lot of dead listener
iterator.remove();
}
}
}
final class SurfaceTextureRegistryEntry
implements TextureRegistry.SurfaceTextureEntry, TextureRegistry.OnTrimMemoryListener {
private final long id;
@NonNull private final SurfaceTextureWrapper textureWrapper;
private boolean released;
@Nullable private OnTrimMemoryListener trimMemoryListener;
@Nullable private OnFrameConsumedListener frameConsumedListener;
SurfaceTextureRegistryEntry(long id, @NonNull SurfaceTexture surfaceTexture) {
this.id = id;
Runnable onFrameConsumed =
() -> {
if (frameConsumedListener != null) {
frameConsumedListener.onFrameConsumed();
}
};
this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture, onFrameConsumed);
// Even though we make sure to unregister the callback before releasing, as of
// Android O, SurfaceTexture has a data race when accessing the callback, so the
// callback may still be called by a stale reference after released==true and
// mNativeView==null.
SurfaceTexture.OnFrameAvailableListener onFrameListener =
texture -> {
if (released || !flutterJNI.isAttached()) {
// Even though we make sure to unregister the callback before releasing, as of
// Android O, SurfaceTexture has a data race when accessing the callback, so the
// callback may still be called by a stale reference after released==true and
// mNativeView==null.
return;
}
textureWrapper.markDirty();
scheduleEngineFrame();
};
// The callback relies on being executed on the UI thread (unsynchronised read of
// mNativeView and also the engine code check for platform thread in
// Shell::OnPlatformViewMarkTextureFrameAvailable), so we explicitly pass a Handler for the
// current thread.
this.surfaceTexture().setOnFrameAvailableListener(onFrameListener, new Handler());
}
@Override
public void onTrimMemory(int level) {
if (trimMemoryListener != null) {
trimMemoryListener.onTrimMemory(level);
}
}
private void removeListener() {
removeOnTrimMemoryListener(this);
}
@NonNull
public SurfaceTextureWrapper textureWrapper() {
return textureWrapper;
}
@Override
@NonNull
public SurfaceTexture surfaceTexture() {
return textureWrapper.surfaceTexture();
}
@Override
public long id() {
return id;
}
@Override
public void release() {
if (released) {
return;
}
Log.v(TAG, "Releasing a SurfaceTexture (" + id + ").");
textureWrapper.release();
unregisterTexture(id);
removeListener();
released = true;
}
@Override
protected void finalize() throws Throwable {
try {
if (released) {
return;
}
handler.post(new TextureFinalizerRunnable(id, flutterJNI));
} finally {
super.finalize();
}
}
@Override
public void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {
frameConsumedListener = listener;
}
@Override
public void setOnTrimMemoryListener(@Nullable OnTrimMemoryListener listener) {
trimMemoryListener = listener;
}
}
static final class TextureFinalizerRunnable implements Runnable {
private final long id;
private final FlutterJNI flutterJNI;
TextureFinalizerRunnable(long id, @NonNull FlutterJNI flutterJNI) {
this.id = id;
this.flutterJNI = flutterJNI;
}
@Override
public void run() {
if (!flutterJNI.isAttached()) {
return;
}
Log.v(TAG, "Releasing a Texture (" + id + ").");
flutterJNI.unregisterTexture(id);
}
}
// Keep a queue of ImageReaders.
// Each ImageReader holds acquired Images.
// When we acquire the next image, close any ImageReaders that don't have any
// more pending images.
@Keep
@TargetApi(API_LEVELS.API_29)
final class ImageReaderSurfaceProducer
implements TextureRegistry.SurfaceProducer,
TextureRegistry.ImageConsumer,
TextureRegistry.OnTrimMemoryListener {
private static final String TAG = "ImageReaderSurfaceProducer";
private static final int MAX_IMAGES = 5;
// Flip when debugging to see verbose logs.
private static final boolean VERBOSE_LOGS = false;
// If we cleanup the ImageReaders on memory pressure it breaks VirtualDisplay
// backed platform views. Disable for now as this is only necessary to work
// around a Samsung-specific Android 14 bug.
private static final boolean CLEANUP_ON_MEMORY_PRESSURE = false;
private final long id;
private boolean released;
// Will be true in tests and on Android API < 33.
private boolean ignoringFence = false;
// The requested width and height are updated by setSize.
private int requestedWidth = 1;
private int requestedHeight = 1;
// Whenever the requested width and height change we set this to be true so we
// create a new ImageReader (inside getSurface) with the correct width and height.
// We use this flag so that we lazily create the ImageReader only when a frame
// will be produced at that size.
private boolean createNewReader = true;
// State held to track latency of various stages.
private long lastDequeueTime = 0;
private long lastQueueTime = 0;
private long lastScheduleTime = 0;
private Object lock = new Object();
// REQUIRED: The following fields must only be accessed when lock is held.
private final LinkedList<PerImageReader> imageReaderQueue = new LinkedList<PerImageReader>();
private final HashMap<ImageReader, PerImageReader> perImageReaders =
new HashMap<ImageReader, PerImageReader>();
private PerImage lastDequeuedImage = null;
private PerImageReader lastReaderDequeuedFrom = null;
/** Internal class: state held per Image produced by ImageReaders. */
private class PerImage {
public final Image image;
public final long queuedTime;
public PerImage(Image image, long queuedTime) {
this.image = image;
this.queuedTime = queuedTime;
}
}
/** Internal class: state held per ImageReader. */
private class PerImageReader {
public final ImageReader reader;
private final LinkedList<PerImage> imageQueue = new LinkedList<PerImage>();
private boolean closed = false;
private final ImageReader.OnImageAvailableListener onImageAvailableListener =
reader -> {
Image image = null;
try {
image = reader.acquireLatestImage();
} catch (IllegalStateException e) {
Log.e(TAG, "onImageAvailable acquireLatestImage failed: " + e);
}
if (image == null) {
return;
}
if (released || closed) {
image.close();
return;
}
onImage(reader, image);
};
public PerImageReader(ImageReader reader) {
this.reader = reader;
reader.setOnImageAvailableListener(onImageAvailableListener, new Handler());
}
PerImage queueImage(Image image) {
if (closed) {
return null;
}
PerImage perImage = new PerImage(image, System.nanoTime());
imageQueue.add(perImage);
// If we fall too far behind we will skip some frames.
while (imageQueue.size() > 2) {
PerImage r = imageQueue.removeFirst();
if (VERBOSE_LOGS) {
Log.i(TAG, "" + reader.hashCode() + " force closed image=" + r.image.hashCode());
}
r.image.close();
}
return perImage;
}
PerImage dequeueImage() {
if (imageQueue.size() == 0) {
return null;
}
PerImage r = imageQueue.removeFirst();
return r;
}
/** returns true if we can prune this reader */
boolean canPrune() {
return imageQueue.size() == 0 && lastReaderDequeuedFrom != this;
}
void close() {
closed = true;
if (VERBOSE_LOGS) {
Log.i(TAG, "Closing reader=" + reader.hashCode());
}
reader.close();
imageQueue.clear();
}
}
double deltaMillis(long deltaNanos) {
double ms = (double) deltaNanos / (double) 1000000.0;
return ms;
}
PerImageReader getOrCreatePerImageReader(ImageReader reader) {
PerImageReader r = perImageReaders.get(reader);
if (r == null) {
r = new PerImageReader(reader);
perImageReaders.put(reader, r);
imageReaderQueue.add(r);
if (VERBOSE_LOGS) {
Log.i(TAG, "imageReaderQueue#=" + imageReaderQueue.size());
}
}
return r;
}
void pruneImageReaderQueue() {
boolean change = false;
// Prune nodes from the head of the ImageReader queue.
while (imageReaderQueue.size() > 1) {
PerImageReader r = imageReaderQueue.peekFirst();
if (!r.canPrune()) {
// No more ImageReaders can be pruned this round.
break;
}
imageReaderQueue.removeFirst();
perImageReaders.remove(r.reader);
r.close();
change = true;
}
if (change && VERBOSE_LOGS) {
Log.i(TAG, "Pruned image reader queue length=" + imageReaderQueue.size());
}
}
void onImage(ImageReader reader, Image image) {
PerImage queuedImage = null;
synchronized (lock) {
PerImageReader perReader = getOrCreatePerImageReader(reader);
queuedImage = perReader.queueImage(image);
}
if (queuedImage == null) {
// We got a late image.
return;
}
if (VERBOSE_LOGS) {
if (lastQueueTime != 0) {
long now = System.nanoTime();
long queueDelta = now - lastQueueTime;
Log.i(
TAG,
""
+ reader.hashCode()
+ " enqueued image="
+ queuedImage.image.hashCode()
+ " queueDelta="
+ deltaMillis(queueDelta));
lastQueueTime = now;
} else {
lastQueueTime = System.nanoTime();
}
}
scheduleEngineFrame();
}
PerImage dequeueImage() {
PerImage r = null;
synchronized (lock) {
for (PerImageReader reader : imageReaderQueue) {
r = reader.dequeueImage();
if (r == null) {
// This reader is probably about to get pruned.
continue;
}
if (VERBOSE_LOGS) {
if (lastDequeueTime != 0) {
long now = System.nanoTime();
long dequeueDelta = now - lastDequeueTime;
long queuedFor = now - r.queuedTime;
long scheduleDelay = now - lastScheduleTime;
Log.i(
TAG,
""
+ reader.reader.hashCode()
+ " dequeued image="
+ r.image.hashCode()
+ " queuedFor= "
+ deltaMillis(queuedFor)
+ " dequeueDelta="
+ deltaMillis(dequeueDelta)
+ " scheduleDelay="
+ deltaMillis(scheduleDelay));
lastDequeueTime = now;
} else {
lastDequeueTime = System.nanoTime();
}
}
if (lastDequeuedImage != null) {
if (VERBOSE_LOGS) {
Log.i(
TAG,
""
+ lastReaderDequeuedFrom.reader.hashCode()
+ " closing image="
+ lastDequeuedImage.image.hashCode());
}
// We must keep the last image dequeued open until we are done presenting
// it. We have just dequeued a new image (r). Close the previously dequeued
// image.
lastDequeuedImage.image.close();
lastDequeuedImage = null;
}
// Remember the last image and reader dequeued from. We do this because we must
// keep both of these alive until we are done presenting the image.
lastDequeuedImage = r;
lastReaderDequeuedFrom = reader;
break;
}
pruneImageReaderQueue();
}
return r;
}
@Override
public void onTrimMemory(int level) {
if (!CLEANUP_ON_MEMORY_PRESSURE) {
return;
}
cleanup();
createNewReader = true;
}
private void releaseInternal() {
cleanup();
released = true;
}
private void cleanup() {
synchronized (lock) {
for (PerImageReader pir : perImageReaders.values()) {
if (lastReaderDequeuedFrom == pir) {
lastReaderDequeuedFrom = null;
}
pir.close();
}
perImageReaders.clear();
if (lastDequeuedImage != null) {
lastDequeuedImage.image.close();
lastDequeuedImage = null;
}
if (lastReaderDequeuedFrom != null) {
lastReaderDequeuedFrom.close();
lastReaderDequeuedFrom = null;
}
imageReaderQueue.clear();
}
}
@TargetApi(API_LEVELS.API_33)
private void waitOnFence(Image image) {
try {
SyncFence fence = image.getFence();
fence.awaitForever();
} catch (IOException e) {
// Drop.
}
}
private void maybeWaitOnFence(Image image) {
if (image == null) {
return;
}
if (ignoringFence) {
return;
}
if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
// The fence API is only available on Android >= 33.
waitOnFence(image);
return;
}
// Log once per ImageTextureEntry.
ignoringFence = true;
Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33");
}
ImageReaderSurfaceProducer(long id) {
this.id = id;
}
@Override
public long id() {
return id;
}
@Override
public void release() {
if (released) {
return;
}
releaseInternal();
unregisterTexture(id);
}
@Override
public void setSize(int width, int height) {
// Clamp to a minimum of 1. A 0x0 texture is a runtime exception in ImageReader.
width = Math.max(1, width);
height = Math.max(1, height);
if (requestedWidth == width && requestedHeight == height) {
// No size change.
return;
}
this.createNewReader = true;
this.requestedHeight = height;
this.requestedWidth = width;
}
@Override
public int getWidth() {
return this.requestedWidth;
}
@Override
public int getHeight() {
return this.requestedHeight;
}
@Override
public Surface getSurface() {
PerImageReader pir = getActiveReader();
if (VERBOSE_LOGS) {
Log.i(TAG, "" + pir.reader.hashCode() + " returning surface to render a new frame.");
}
return pir.reader.getSurface();
}
@Override
public void scheduleFrame() {
if (VERBOSE_LOGS) {
long now = System.nanoTime();
if (lastScheduleTime != 0) {
long delta = now - lastScheduleTime;
Log.v(TAG, "scheduleFrame delta=" + deltaMillis(delta));
}
lastScheduleTime = now;
}
scheduleEngineFrame();
}
@Override
@TargetApi(API_LEVELS.API_29)
public Image acquireLatestImage() {
PerImage r = dequeueImage();
if (r == null) {
return null;
}
maybeWaitOnFence(r.image);
return r.image;
}
private PerImageReader getActiveReader() {
synchronized (lock) {
if (createNewReader) {
createNewReader = false;
// Create a new ImageReader and add it to the queue.
ImageReader reader = createImageReader();
if (VERBOSE_LOGS) {
Log.i(
TAG,
"" + reader.hashCode() + " created w=" + requestedWidth + " h=" + requestedHeight);
}
return getOrCreatePerImageReader(reader);
}
return imageReaderQueue.peekLast();
}
}
@Override
protected void finalize() throws Throwable {
try {
if (released) {
return;
}
releaseInternal();
handler.post(new TextureFinalizerRunnable(id, flutterJNI));
} finally {
super.finalize();
}
}
@TargetApi(API_LEVELS.API_33)
private ImageReader createImageReader33() {
final ImageReader.Builder builder = new ImageReader.Builder(requestedWidth, requestedHeight);
// Allow for double buffering.
builder.setMaxImages(MAX_IMAGES);
// Use PRIVATE image format so that we can support video decoding.
// TODO(johnmccutchan): Should we always use PRIVATE here? It may impact our ability to
// read back texture data. If we don't always want to use it, how do we decide when to
// use it or not? Perhaps PlatformViews can indicate if they may contain DRM'd content.
// I need to investigate how PRIVATE impacts our ability to take screenshots or capture
// the output of Flutter application.
builder.setImageFormat(ImageFormat.PRIVATE);
// Hint that consumed images will only be read by GPU.
builder.setUsage(HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
final ImageReader reader = builder.build();
return reader;
}
@TargetApi(API_LEVELS.API_29)
private ImageReader createImageReader29() {
final ImageReader reader =
ImageReader.newInstance(
requestedWidth,
requestedHeight,
ImageFormat.PRIVATE,
MAX_IMAGES,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
return reader;
}
private ImageReader createImageReader() {
if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
return createImageReader33();
} else if (Build.VERSION.SDK_INT >= API_LEVELS.API_29) {
return createImageReader29();
}
throw new UnsupportedOperationException(
"ImageReaderPlatformViewRenderTarget requires API version 29+");
}
@VisibleForTesting
public void disableFenceForTest() {
// Roboelectric's implementation of SyncFence is borked.
ignoringFence = true;
}
@VisibleForTesting
public int numImageReaders() {
synchronized (lock) {
return imageReaderQueue.size();
}
}
@VisibleForTesting
public int numImages() {
int r = 0;
synchronized (lock) {
for (PerImageReader reader : imageReaderQueue) {
r += reader.imageQueue.size();
}
}
return r;
}
}
@Keep
final class ImageTextureRegistryEntry
implements TextureRegistry.ImageTextureEntry, TextureRegistry.ImageConsumer {
private static final String TAG = "ImageTextureRegistryEntry";
private final long id;
private boolean released;
private boolean ignoringFence = false;
private Image image;
ImageTextureRegistryEntry(long id) {
this.id = id;
}
@Override
public long id() {
return id;
}
@Override
public void release() {
if (released) {
return;
}
released = true;
if (image != null) {
image.close();
image = null;
}
unregisterTexture(id);
}
@Override
public void pushImage(Image image) {
if (released) {
return;
}
Image toClose;
synchronized (this) {
toClose = this.image;
this.image = image;
}
// Close the previously pushed buffer.
if (toClose != null) {
Log.e(TAG, "Dropping PlatformView Frame");
toClose.close();
}
if (image != null) {
scheduleEngineFrame();
}
}
@TargetApi(API_LEVELS.API_33)
private void waitOnFence(Image image) {
try {
SyncFence fence = image.getFence();
fence.awaitForever();
} catch (IOException e) {
// Drop.
}
}
@TargetApi(API_LEVELS.API_29)
private void maybeWaitOnFence(Image image) {
if (image == null) {
return;
}
if (ignoringFence) {
return;
}
if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
// The fence API is only available on Android >= 33.
waitOnFence(image);
return;
}
// Log once per ImageTextureEntry.
ignoringFence = true;
Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33");
}
@Override
@TargetApi(API_LEVELS.API_29)
public Image acquireLatestImage() {
Image r;
synchronized (this) {
r = this.image;
this.image = null;
}
maybeWaitOnFence(r);
return r;
}
@Override
protected void finalize() throws Throwable {
try {
if (released) {
return;
}
if (image != null) {
// Be sure to finalize any cached image.
image.close();
image = null;
}
released = true;
handler.post(new TextureFinalizerRunnable(id, flutterJNI));
} finally {
super.finalize();
}
}
}
// ------ END TextureRegistry IMPLEMENTATION ----
/**
* Notifies Flutter that the given {@code surface} was created and is available for Flutter
* rendering.
*
* <p>If called more than once, the current native resources are released. This can be undesired
* if the Engine expects to reuse this surface later. For example, this is true when platform
* views are displayed in a frame, and then removed in the next frame.
*
* <p>To avoid releasing the current surface resources, set {@code keepCurrentSurface} to true.
*
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
* android.view.TextureView.SurfaceTextureListener}
*
* @param surface The render surface.
* @param onlySwap True if the current active surface should not be detached.
*/
public void startRenderingToSurface(@NonNull Surface surface, boolean onlySwap) {
if (!onlySwap) {
// Stop rendering to the surface releases the associated native resources, which causes
// a glitch when toggling between rendering to an image view (hybrid composition) and
// rendering directly to a Surface or Texture view. For more,
// https://github.com/flutter/flutter/issues/95343
stopRenderingToSurface();
}
this.surface = surface;
if (onlySwap) {
// In the swap case we are just swapping the surface that we render to.
flutterJNI.onSurfaceWindowChanged(surface);
} else {
// In the non-swap case we are creating a new surface to render to.
flutterJNI.onSurfaceCreated(surface);
}
}
/**
* Swaps the {@link Surface} used to render the current frame.
*
* <p>In hybrid composition, the root surfaces changes from {@link
* android.view.SurfaceHolder#getSurface()} to {@link android.media.ImageReader#getSurface()} when
* a platform view is in the current frame.
*/
public void swapSurface(@NonNull Surface surface) {
this.surface = surface;
flutterJNI.onSurfaceWindowChanged(surface);
}
/**
* Notifies Flutter that a {@code surface} previously registered with {@link
* #startRenderingToSurface(Surface, boolean)} has changed size to the given {@code width} and
* {@code height}.
*
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
* android.view.TextureView.SurfaceTextureListener}
*/
public void surfaceChanged(int width, int height) {
flutterJNI.onSurfaceChanged(width, height);
}
/**
* Notifies Flutter that a {@code surface} previously registered with {@link
* #startRenderingToSurface(Surface, boolean)} has been destroyed and needs to be released and
* cleaned up on the Flutter side.
*
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
* android.view.TextureView.SurfaceTextureListener}
*/
public void stopRenderingToSurface() {
if (surface != null) {
flutterJNI.onSurfaceDestroyed();
// TODO(mattcarroll): the source of truth for this call should be FlutterJNI, which is
// where the call to onFlutterUiDisplayed() comes from. However, no such native callback
// exists yet, so until the engine and FlutterJNI are configured to call us back when
// rendering stops, we will manually monitor that change here.
if (isDisplayingFlutterUi) {
flutterUiDisplayListener.onFlutterUiNoLongerDisplayed();
}
isDisplayingFlutterUi = false;
surface = null;
}
}
/**
* Notifies Flutter that the viewport metrics, e.g. window height and width, have changed.
*
* <p>If the width, height, or devicePixelRatio are less than or equal to 0, this update is
* ignored.
*
* @param viewportMetrics The metrics to send to the Dart application.
*/
public void setViewportMetrics(@NonNull ViewportMetrics viewportMetrics) {
// We might get called with just the DPR if width/height aren't available yet.
// Just ignore, as it will get called again when width/height are set.
if (!viewportMetrics.validate()) {
return;
}
Log.v(
TAG,
"Setting viewport metrics\n"
+ "Size: "
+ viewportMetrics.width
+ " x "
+ viewportMetrics.height
+ "\n"
+ "Padding - L: "
+ viewportMetrics.viewPaddingLeft
+ ", T: "
+ viewportMetrics.viewPaddingTop
+ ", R: "
+ viewportMetrics.viewPaddingRight
+ ", B: "
+ viewportMetrics.viewPaddingBottom
+ "\n"
+ "Insets - L: "
+ viewportMetrics.viewInsetLeft
+ ", T: "
+ viewportMetrics.viewInsetTop
+ ", R: "
+ viewportMetrics.viewInsetRight
+ ", B: "
+ viewportMetrics.viewInsetBottom
+ "\n"
+ "System Gesture Insets - L: "
+ viewportMetrics.systemGestureInsetLeft
+ ", T: "
+ viewportMetrics.systemGestureInsetTop
+ ", R: "
+ viewportMetrics.systemGestureInsetRight
+ ", B: "
+ viewportMetrics.systemGestureInsetRight
+ "\n"
+ "Display Features: "
+ viewportMetrics.displayFeatures.size());
int[] displayFeaturesBounds = new int[viewportMetrics.displayFeatures.size() * 4];
int[] displayFeaturesType = new int[viewportMetrics.displayFeatures.size()];
int[] displayFeaturesState = new int[viewportMetrics.displayFeatures.size()];
for (int i = 0; i < viewportMetrics.displayFeatures.size(); i++) {
DisplayFeature displayFeature = viewportMetrics.displayFeatures.get(i);
displayFeaturesBounds[4 * i] = displayFeature.bounds.left;
displayFeaturesBounds[4 * i + 1] = displayFeature.bounds.top;
displayFeaturesBounds[4 * i + 2] = displayFeature.bounds.right;
displayFeaturesBounds[4 * i + 3] = displayFeature.bounds.bottom;
displayFeaturesType[i] = displayFeature.type.encodedValue;
displayFeaturesState[i] = displayFeature.state.encodedValue;
}
flutterJNI.setViewportMetrics(
viewportMetrics.devicePixelRatio,
viewportMetrics.width,
viewportMetrics.height,
viewportMetrics.viewPaddingTop,
viewportMetrics.viewPaddingRight,
viewportMetrics.viewPaddingBottom,
viewportMetrics.viewPaddingLeft,
viewportMetrics.viewInsetTop,
viewportMetrics.viewInsetRight,
viewportMetrics.viewInsetBottom,
viewportMetrics.viewInsetLeft,
viewportMetrics.systemGestureInsetTop,
viewportMetrics.systemGestureInsetRight,
viewportMetrics.systemGestureInsetBottom,
viewportMetrics.systemGestureInsetLeft,
viewportMetrics.physicalTouchSlop,
displayFeaturesBounds,
displayFeaturesType,
displayFeaturesState);
}
// TODO(mattcarroll): describe the native behavior that this invokes
// TODO(mattcarroll): determine if this is nullable or nonnull
public Bitmap getBitmap() {
return flutterJNI.getBitmap();
}
// TODO(mattcarroll): describe the native behavior that this invokes
public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) {
flutterJNI.dispatchPointerDataPacket(buffer, position);
}
// TODO(mattcarroll): describe the native behavior that this invokes
private void registerTexture(long textureId, @NonNull SurfaceTextureWrapper textureWrapper) {
flutterJNI.registerTexture(textureId, textureWrapper);
}
private void registerImageTexture(
long textureId, @NonNull TextureRegistry.ImageConsumer imageTexture) {
flutterJNI.registerImageTexture(textureId, imageTexture);
}
private void scheduleEngineFrame() {
flutterJNI.scheduleFrame();
}
// TODO(mattcarroll): describe the native behavior that this invokes
private void markTextureFrameAvailable(long textureId) {
flutterJNI.markTextureFrameAvailable(textureId);
}
// TODO(mattcarroll): describe the native behavior that this invokes
private void unregisterTexture(long textureId) {
flutterJNI.unregisterTexture(textureId);
}
// TODO(mattcarroll): describe the native behavior that this invokes
public boolean isSoftwareRenderingEnabled() {
return flutterJNI.getIsSoftwareRenderingEnabled();
}
// TODO(mattcarroll): describe the native behavior that this invokes
public void setAccessibilityFeatures(int flags) {
flutterJNI.setAccessibilityFeatures(flags);
}
// TODO(mattcarroll): describe the native behavior that this invokes
public void setSemanticsEnabled(boolean enabled) {
flutterJNI.setSemanticsEnabled(enabled);
}
// TODO(mattcarroll): describe the native behavior that this invokes
public void dispatchSemanticsAction(
int nodeId, int action, @Nullable ByteBuffer args, int argsPosition) {
flutterJNI.dispatchSemanticsAction(nodeId, action, args, argsPosition);
}
/**
* Mutable data structure that holds all viewport metrics properties that Flutter cares about.
*
* <p>All distance measurements, e.g., width, height, padding, viewInsets, are measured in device
* pixels, not logical pixels.
*/
public static final class ViewportMetrics {
/** A value that indicates the setting has not been set. */
public static final int unsetValue = -1;
public float devicePixelRatio = 1.0f;
public int width = 0;
public int height = 0;
public int viewPaddingTop = 0;
public int viewPaddingRight = 0;
public int viewPaddingBottom = 0;
public int viewPaddingLeft = 0;
public int viewInsetTop = 0;
public int viewInsetRight = 0;
public int viewInsetBottom = 0;
public int viewInsetLeft = 0;
public int systemGestureInsetTop = 0;
public int systemGestureInsetRight = 0;
public int systemGestureInsetBottom = 0;
public int systemGestureInsetLeft = 0;
public int physicalTouchSlop = unsetValue;
/**
* Whether this instance contains valid metrics for the Flutter application.
*
* @return True if width, height, and devicePixelRatio are > 0; false otherwise.
*/
boolean validate() {
return width > 0 && height > 0 && devicePixelRatio > 0;
}
public List<DisplayFeature> displayFeatures = new ArrayList<>();
}
/**
* Description of a physical feature on the display.
*
* <p>A display feature is a distinctive physical attribute located within the display panel of
* the device. It can intrude into the application window space and create a visual distortion,
* visual or touch discontinuity, make some area invisible or create a logical divider or
* separation in the screen space.
*
* <p>Based on {@link androidx.window.layout.DisplayFeature}, with added support for cutouts.
*/
public static final class DisplayFeature {
public final Rect bounds;
public final DisplayFeatureType type;
public final DisplayFeatureState state;
public DisplayFeature(Rect bounds, DisplayFeatureType type, DisplayFeatureState state) {
this.bounds = bounds;
this.type = type;
this.state = state;
}
public DisplayFeature(Rect bounds, DisplayFeatureType type) {
this.bounds = bounds;
this.type = type;
this.state = DisplayFeatureState.UNKNOWN;
}
}
/**
* Types of display features that can appear on the viewport.
*
* <p>Some, like {@link #FOLD}, can be reported without actually occluding the screen. They are
* useful for knowing where the display is bent or has a crease. The {@link DisplayFeature#bounds}
* can be 0-width in such cases.
*/
public enum DisplayFeatureType {
/**
* Type of display feature not yet known to Flutter. This can happen if WindowManager is updated
* with new types. The {@link DisplayFeature#bounds} is the only known property.
*/
UNKNOWN(0),
/**
* A fold in the flexible display that does not occlude the screen. Corresponds to {@link
* androidx.window.layout.FoldingFeature.OcclusionType#NONE}
*/
FOLD(1),
/**
* Splits the display in two separate panels that can fold. Occludes the screen. Corresponds to
* {@link androidx.window.layout.FoldingFeature.OcclusionType#FULL}
*/
HINGE(2),
/**
* Area of the screen that usually houses cameras or sensors. Occludes the screen. Corresponds
* to {@link android.view.DisplayCutout}
*/
CUTOUT(3);
public final int encodedValue;
DisplayFeatureType(int encodedValue) {
this.encodedValue = encodedValue;
}
}
/**
* State of the display feature.
*
* <p>For foldables, the state is the posture. For cutouts, this property is {@link #UNKNOWN}
*/
public enum DisplayFeatureState {
/** The display feature is a cutout or this state is new and not yet known to Flutter. */
UNKNOWN(0),
/**
* The foldable device is completely open. The screen space that is presented to the user is
* flat. Corresponds to {@link androidx.window.layout.FoldingFeature.State#FLAT}
*/
POSTURE_FLAT(1),
/**
* The foldable device's hinge is in an intermediate position between opened and closed state.
* There is a non-flat angle between parts of the flexible screen or between physical display
* panels. Corresponds to {@link androidx.window.layout.FoldingFeature.State#HALF_OPENED}
*/
POSTURE_HALF_OPENED(2);
public final int encodedValue;
DisplayFeatureState(int encodedValue) {
this.encodedValue = encodedValue;
}
}
}