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 83b8ddbb478a6..cbab9e99ef593 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; @@ -418,10 +419,9 @@ final class ImageReaderSurfaceProducer // 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; + // 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; @@ -429,6 +429,8 @@ final class ImageReaderSurfaceProducer // 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; @@ -442,6 +444,7 @@ 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. @@ -659,9 +662,15 @@ PerImage dequeueImage() { @Override public void onTrimMemory(int level) { - if (!CLEANUP_ON_MEMORY_PRESSURE) { + if (!trimOnMemoryPressure) { + return; + } + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { return; } + synchronized (lock) { + numTrims++; + } cleanup(); createNewReader = true; } @@ -878,6 +887,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 bc36ed3075f73..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,13 +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.