diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 6b8b0c44aafde..b7431c8bdb8b9 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -578,6 +578,7 @@ void onPostResume() { ensureAlive(); if (flutterEngine != null) { updateSystemUiOverlays(); + flutterEngine.getPlatformViewsController().onResume(); } else { Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity."); } @@ -921,6 +922,7 @@ void onTrimMemory(int level) { flutterEngine.getSystemChannel().sendMemoryPressureWarning(); } flutterEngine.getRenderer().onTrimMemory(level); + flutterEngine.getPlatformViewsController().onTrimMemory(level); } } 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 9fb0b12244d32..0d174d93e132a 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -5,6 +5,7 @@ package io.flutter.embedding.engine.renderer; import android.annotation.TargetApi; +import android.content.ComponentCallbacks2; import android.graphics.Bitmap; import android.graphics.ImageFormat; import android.graphics.Rect; @@ -361,15 +362,44 @@ public void run() { final class ImageReaderSurfaceProducer implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer { private static final String TAG = "ImageReaderSurfaceProducer"; - private static final int MAX_IMAGES = 4; + private static final int MAX_IMAGES = 5; + + // Flip when debugging to see verbose logs. + private static final boolean VERBOSE_LOGS = false; + + // We must always cleanup on memory pressure on Android 14 due to a bug in Android. + // It is safe to do on all versions so we unconditionally have this set to true. + private static final boolean CLEANUP_ON_MEMORY_PRESSURE = true; private final long id; private boolean released; private boolean ignoringFence = false; - private int requestedWidth = 0; - private int requestedHeight = 0; + private boolean trimOnMemoryPressure = CLEANUP_ON_MEMORY_PRESSURE; + + // 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 int numTrims = 0; + + private Object lock = new Object(); + // REQUIRED: The following fields must only be accessed when lock is held. + private final ArrayDeque imageReaderQueue = new ArrayDeque(); + private final HashMap perImageReaders = + new HashMap(); + private PerImage lastDequeuedImage = null; + private PerImageReader lastReaderDequeuedFrom = null; /** Internal class: state held per image produced by image readers. */ private class PerImage { @@ -528,39 +558,44 @@ private void maybeCloseReader(ImageReader reader) { reader.close(); } - private void maybeCreateReader() { - synchronized (this) { - if (this.activeReader != null) { - return; - } - this.activeReader = createImageReader(); - } - } - - /** Invoked for each method that is available. */ - private void onImage(PerImage image) { - if (released) { + @Override + public void onTrimMemory(int level) { + if (!trimOnMemoryPressure) { return; } - PerImage toClose; - synchronized (this) { - if (this.readersToClose.contains(image.reader)) { - Log.i(TAG, "Skipped frame because resize is in flight."); - image.close(); - return; - } - toClose = this.lastProducedImage; - this.lastProducedImage = image; + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + return; } - // Close the previously pushed buffer. - if (toClose != null) { - Log.i(TAG, "Dropping rendered frame that was not acquired in time."); - toClose.close(); + synchronized (lock) { + numTrims++; } - if (image != null) { - // Mark that we have a new frame available. Eventually the raster thread will - // call acquireLatestImage. - markTextureFrameAvailable(id); + 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(); } } @@ -662,8 +697,28 @@ public void disableFenceForTest() { } @VisibleForTesting - public int readersToCloseSize() { - return readersToClose.size(); + public int numImageReaders() { + synchronized (lock) { + return imageReaderQueue.size(); + } + } + + @VisibleForTesting + public int numTrims() { + synchronized (lock) { + return numTrims; + } + } + + @VisibleForTesting + public int numImages() { + int r = 0; + synchronized (lock) { + for (PerImageReader reader : imageReaderQueue) { + r += reader.imageQueue.size(); + } + } + return r; } } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 845baecf76cea..a736f0c2370cc 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -8,6 +8,7 @@ import static android.view.MotionEvent.PointerProperties; import android.annotation.TargetApi; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.MutableContextWrapper; import android.os.Build; @@ -1056,6 +1057,24 @@ private void diposeAllViews() { } } + // Invoked when the Android system is requesting we reduce memory usage. + public void onTrimMemory(int level) { + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + return; + } + for (VirtualDisplayController vdc : vdControllers.values()) { + vdc.clearSurface(); + } + } + + // Called after the application has been resumed. + // This is where we undo whatever may have been done in onTrimMemory. + public void onResume() { + for (VirtualDisplayController vdc : vdControllers.values()) { + vdc.resetSurface(); + } + } + /** * Disposes a single * diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 1cb44373b1bdf..9a3012d443800 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -261,6 +261,49 @@ public void dispatchTouchEvent(MotionEvent event) { presentation.dispatchTouchEvent(event); } + public void clearSurface() { + virtualDisplay.setSurface(null); + } + + public void resetSurface() { + final int width = getRenderTargetWidth(); + final int height = getRenderTargetHeight(); + final boolean isFocused = getView().isFocused(); + final SingleViewPresentation.PresentationState presentationState = presentation.detachState(); + + // We detach the surface to prevent it being destroyed when releasing the vd. + virtualDisplay.setSurface(null); + virtualDisplay.release(); + final DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + int flags = 0; + virtualDisplay = + displayManager.createVirtualDisplay( + "flutter-vd#" + viewId, + width, + height, + densityDpi, + renderTarget.getSurface(), + flags, + callback, + null /* handler */); + // Create a new SingleViewPresentation and show() it before we cancel() the existing + // presentation. Calling show() and cancel() in this order fixes + // https://github.com/flutter/flutter/issues/26345 and maintains seamless transition + // of the contents of the presentation. + SingleViewPresentation newPresentation = + new SingleViewPresentation( + context, + virtualDisplay.getDisplay(), + accessibilityEventsDelegate, + presentationState, + focusChangeListener, + isFocused); + newPresentation.show(); + presentation.cancel(); + presentation = newPresentation; + } + static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener { static void schedule(View view, Runnable runnable) { OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index 3330d34f822f5..1b905e93f329d 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -596,7 +596,109 @@ public void ImageReaderSurfaceProducerHandlesLateFrameWhenResizeInflight() { // Acquire the new image. assertNotNull(texture.acquireLatestImage()); - // We will have no pending readers to close. - assertEquals(0, texture.readersToCloseSize()); + // Returns null image when no more images are queued. + assertNull(texture.acquireLatestImage()); + assertEquals(1, texture.numImageReaders()); + assertEquals(0, texture.numImages()); + } + + @Test + public void ImageReaderSurfaceProducerTrimMemoryCallback() { + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer.ImageReaderSurfaceProducer texture = + flutterRenderer.new ImageReaderSurfaceProducer(0); + texture.disableFenceForTest(); + + // Returns a null image when one hasn't been produced. + assertNull(texture.acquireLatestImage()); + + // Give the texture an initial size. + texture.setSize(1, 1); + + // Grab the surface so we can render a frame at 1x1 after resizing. + Surface surface = texture.getSurface(); + assertNotNull(surface); + Canvas canvas = surface.lockHardwareCanvas(); + canvas.drawARGB(255, 255, 0, 0); + surface.unlockCanvasAndPost(canvas); + + // Let callbacks run, this will produce a single frame. + shadowOf(Looper.getMainLooper()).idle(); + + assertEquals(1, texture.numImageReaders()); + assertEquals(1, texture.numImages()); + + // Invoke the onTrimMemory callback with level 0. + // This should do nothing. + texture.onTrimMemory(0); + shadowOf(Looper.getMainLooper()).idle(); + + assertEquals(1, texture.numImageReaders()); + assertEquals(1, texture.numImages()); + assertEquals(0, texture.numTrims()); + + // Invoke the onTrimMemory callback with level 40. + // This should result in a trim. + texture.onTrimMemory(40); + shadowOf(Looper.getMainLooper()).idle(); + + assertEquals(0, texture.numImageReaders()); + assertEquals(0, texture.numImages()); + assertEquals(1, texture.numTrims()); + + // Request the surface, this should result in a new image reader. + surface = texture.getSurface(); + assertEquals(1, texture.numImageReaders()); + assertEquals(0, texture.numImages()); + assertEquals(1, texture.numTrims()); + + // Render an image. + canvas = surface.lockHardwareCanvas(); + canvas.drawARGB(255, 255, 0, 0); + surface.unlockCanvasAndPost(canvas); + + // Let callbacks run, this will produce a single frame. + shadowOf(Looper.getMainLooper()).idle(); + + assertEquals(1, texture.numImageReaders()); + assertEquals(1, texture.numImages()); + assertEquals(1, texture.numTrims()); + } + + // A 0x0 ImageReader is a runtime error. + @Test + public void ImageReaderSurfaceProducerClampsWidthAndHeightTo1() { + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer.ImageReaderSurfaceProducer texture = + flutterRenderer.new ImageReaderSurfaceProducer(0); + + // Default values. + assertEquals(texture.getWidth(), 1); + assertEquals(texture.getHeight(), 1); + + // Try setting width and height to 0. + texture.setSize(0, 0); + + // Ensure we can still create/get a surface without an exception being raised. + assertNotNull(texture.getSurface()); + + // Expect clamp to 1. + assertEquals(texture.getWidth(), 1); + assertEquals(texture.getHeight(), 1); + } + + @Test + public void SurfaceTextureSurfaceProducerCreatesAConnectedTexture() { + // Force creating a SurfaceTextureSurfaceProducer regardless of Android API version. + FlutterRenderer.debugForceSurfaceProducerGlTextures = true; + + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + + flutterRenderer.startRenderingToSurface(fakeSurface, false); + + // Verify behavior under test. + assertEquals(producer.id(), 0); + verify(fakeFlutterJNI, times(1)).registerTexture(eq(producer.id()), any()); } }