diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 2f1156704b982..1f6118bfe56c4 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -583,6 +583,7 @@ void onPostResume() { ensureAlive(); if (flutterEngine != null) { updateSystemUiOverlays(); + flutterEngine.getPlatformViewsController().onResume(); } else { Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity."); } @@ -1020,6 +1021,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 673f58bb25d6b..ee1f633275b46 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -7,6 +7,7 @@ import static io.flutter.Build.API_LEVELS; import android.annotation.TargetApi; +import android.content.ComponentCallbacks2; import android.graphics.Bitmap; import android.graphics.ImageFormat; import android.graphics.Rect; @@ -29,11 +30,11 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; +import java.util.ArrayDeque; 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; @@ -415,12 +416,18 @@ final class ImageReaderSurfaceProducer // 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; // Will be true in tests and on Android API < 33. private boolean ignoringFence = false; + private boolean trimOnMemoryPressure = CLEANUP_ON_MEMORY_PRESSURE; + // The requested width and height are updated by setSize. private int requestedWidth = 1; private int requestedHeight = 1; @@ -434,10 +441,11 @@ final class ImageReaderSurfaceProducer 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 LinkedList imageReaderQueue = new LinkedList(); + private final ArrayDeque imageReaderQueue = new ArrayDeque(); private final HashMap perImageReaders = new HashMap(); private PerImage lastDequeuedImage = null; @@ -457,7 +465,7 @@ public PerImage(Image image, long queuedTime) { /** Internal class: state held per ImageReader. */ private class PerImageReader { public final ImageReader reader; - private final LinkedList imageQueue = new LinkedList(); + private final ArrayDeque imageQueue = new ArrayDeque(); private boolean closed = false; private final ImageReader.OnImageAvailableListener onImageAvailableListener = @@ -651,6 +659,15 @@ PerImage dequeueImage() { @Override public void onTrimMemory(int level) { + if (!trimOnMemoryPressure) { + return; + } + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + return; + } + synchronized (lock) { + numTrims++; + } cleanup(); createNewReader = true; } @@ -867,6 +884,13 @@ public int numImageReaders() { } } + @VisibleForTesting + public int numTrims() { + synchronized (lock) { + return numTrims; + } + } + @VisibleForTesting public int numImages() { int r = 0; diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 6e2b2ab250f82..093be1566cb4e 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -9,6 +9,7 @@ import static io.flutter.Build.API_LEVELS; import android.annotation.TargetApi; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.MutableContextWrapper; import android.os.Build; @@ -1053,6 +1054,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 5de42299e00c4..cdde0687a4a99 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -284,6 +284,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 50f83d5fb8e0a..f19da788d3759 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 @@ -647,12 +647,41 @@ public void ImageReaderSurfaceProducerTrimMemoryCallback() { assertEquals(1, texture.numImageReaders()); assertEquals(1, texture.numImages()); - // Invoke the onTrimMemory callback. + // 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.