Skip to content

Commit 49d6805

Browse files
author
Emmanuel Garcia
authored
Ensure all images are closed in FlutterImageView (flutter#20842)
1 parent a762143 commit 49d6805

File tree

2 files changed

+113
-33
lines changed

2 files changed

+113
-33
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterImageView.java

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.flutter.embedding.android;
66

7-
import android.annotation.SuppressLint;
87
import android.annotation.TargetApi;
98
import android.content.Context;
109
import android.graphics.Bitmap;
@@ -15,13 +14,16 @@
1514
import android.media.Image;
1615
import android.media.Image.Plane;
1716
import android.media.ImageReader;
17+
import android.util.AttributeSet;
1818
import android.view.Surface;
1919
import android.view.View;
2020
import androidx.annotation.NonNull;
2121
import androidx.annotation.Nullable;
2222
import androidx.annotation.VisibleForTesting;
2323
import io.flutter.embedding.engine.renderer.FlutterRenderer;
2424
import io.flutter.embedding.engine.renderer.RenderSurface;
25+
import java.util.LinkedList;
26+
import java.util.Queue;
2527

2628
/**
2729
* Paints a Flutter UI provided by an {@link android.media.ImageReader} onto a {@link
@@ -35,11 +37,10 @@
3537
* an {@link android.media.Image} and renders it to the {@link android.graphics.Canvas} in {@code
3638
* onDraw}.
3739
*/
38-
@SuppressLint("ViewConstructor")
3940
@TargetApi(19)
4041
public class FlutterImageView extends View implements RenderSurface {
4142
@NonNull private ImageReader imageReader;
42-
@Nullable private Image nextImage;
43+
@Nullable private Queue<Image> imageQueue;
4344
@Nullable private Image currentImage;
4445
@Nullable private Bitmap currentBitmap;
4546
@Nullable private FlutterRenderer flutterRenderer;
@@ -70,17 +71,24 @@ public enum SurfaceKind {
7071
* the Flutter UI.
7172
*/
7273
public FlutterImageView(@NonNull Context context, int width, int height, SurfaceKind kind) {
73-
super(context, null);
74-
this.imageReader = createImageReader(width, height);
75-
this.kind = kind;
76-
init();
74+
this(context, createImageReader(width, height), kind);
75+
}
76+
77+
public FlutterImageView(@NonNull Context context) {
78+
this(context, 1, 1, SurfaceKind.background);
79+
}
80+
81+
public FlutterImageView(@NonNull Context context, @NonNull AttributeSet attrs) {
82+
this(context, 1, 1, SurfaceKind.background);
7783
}
7884

7985
@VisibleForTesting
80-
FlutterImageView(@NonNull Context context, @NonNull ImageReader imageReader, SurfaceKind kind) {
86+
/*package*/ FlutterImageView(
87+
@NonNull Context context, @NonNull ImageReader imageReader, SurfaceKind kind) {
8188
super(context, null);
8289
this.imageReader = imageReader;
8390
this.kind = kind;
91+
this.imageQueue = new LinkedList<>();
8492
init();
8593
}
8694

@@ -150,12 +158,14 @@ public void detachFromRenderer() {
150158
// attached to the renderer again.
151159
acquireLatestImage();
152160
// Clear drawings.
153-
pendingImages = 0;
154161
currentBitmap = null;
155-
if (nextImage != null) {
156-
nextImage.close();
157-
nextImage = null;
162+
163+
// Close the images in the queue and clear the queue.
164+
for (final Image image : imageQueue) {
165+
image.close();
158166
}
167+
imageQueue.clear();
168+
// Close and clear the current image if any.
159169
if (currentImage != null) {
160170
currentImage.close();
161171
currentImage = null;
@@ -168,7 +178,10 @@ public void pause() {
168178
// Not supported.
169179
}
170180

171-
/** Acquires the next image to be drawn to the {@link android.graphics.Canvas}. */
181+
/**
182+
* Acquires the next image to be drawn to the {@link android.graphics.Canvas}. Returns true if
183+
* there's an image available in the queue.
184+
*/
172185
@TargetApi(19)
173186
public boolean acquireLatestImage() {
174187
if (!isAttachedToFlutterRenderer) {
@@ -182,14 +195,14 @@ public boolean acquireLatestImage() {
182195
// While the engine will also stop producing frames, there is a race condition.
183196
//
184197
// To avoid exceptions, check if a new image can be acquired.
185-
if (pendingImages < imageReader.getMaxImages()) {
186-
nextImage = imageReader.acquireLatestImage();
187-
if (nextImage != null) {
188-
pendingImages++;
198+
if (imageQueue.size() < imageReader.getMaxImages()) {
199+
final Image image = imageReader.acquireLatestImage();
200+
if (image != null) {
201+
imageQueue.add(image);
189202
}
190203
}
191204
invalidate();
192-
return nextImage != null;
205+
return !imageQueue.isEmpty();
193206
}
194207

195208
/** Creates a new image reader with the provided size. */
@@ -200,15 +213,10 @@ public void resizeIfNeeded(int width, int height) {
200213
if (width == imageReader.getWidth() && height == imageReader.getHeight()) {
201214
return;
202215
}
203-
// Close resources.
204-
if (nextImage != null) {
205-
nextImage.close();
206-
nextImage = null;
207-
}
208-
if (currentImage != null) {
209-
currentImage.close();
210-
currentImage = null;
211-
}
216+
imageQueue.clear();
217+
currentImage = null;
218+
// Close all the resources associated with the image reader,
219+
// including the images.
212220
imageReader.close();
213221
// Image readers cannot be resized once created.
214222
imageReader = createImageReader(width, height);
@@ -218,16 +226,14 @@ public void resizeIfNeeded(int width, int height) {
218226
@Override
219227
protected void onDraw(Canvas canvas) {
220228
super.onDraw(canvas);
221-
if (nextImage != null) {
229+
230+
if (!imageQueue.isEmpty()) {
222231
if (currentImage != null) {
223232
currentImage.close();
224-
pendingImages--;
225233
}
226-
currentImage = nextImage;
227-
nextImage = null;
234+
currentImage = imageQueue.poll();
228235
updateCurrentBitmap();
229236
}
230-
231237
if (currentBitmap != null) {
232238
canvas.drawBitmap(currentBitmap, 0, 0, null);
233239
}
@@ -238,6 +244,7 @@ private void updateCurrentBitmap() {
238244
if (android.os.Build.VERSION.SDK_INT >= 29) {
239245
final HardwareBuffer buffer = currentImage.getHardwareBuffer();
240246
currentBitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
247+
buffer.close();
241248
} else {
242249
final Plane[] imagePlanes = currentImage.getPlanes();
243250
if (imagePlanes.length != 1) {
@@ -255,7 +262,6 @@ private void updateCurrentBitmap() {
255262
Bitmap.createBitmap(
256263
desiredWidth, desiredHeight, android.graphics.Bitmap.Config.ARGB_8888);
257264
}
258-
259265
currentBitmap.copyPixelsFromBuffer(imagePlane.getBuffer());
260266
}
261267
}

shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
import static org.mockito.Mockito.verify;
1313
import static org.mockito.Mockito.when;
1414

15+
import android.annotation.SuppressLint;
1516
import android.annotation.TargetApi;
1617
import android.content.Context;
1718
import android.content.res.Configuration;
1819
import android.content.res.Resources;
20+
import android.graphics.Canvas;
1921
import android.graphics.Insets;
2022
import android.media.Image;
23+
import android.media.Image.Plane;
2124
import android.media.ImageReader;
2225
import android.view.View;
2326
import android.view.ViewGroup;
@@ -550,6 +553,77 @@ public void flutterImageView_acquiresMaxImagesAtMost() {
550553
verify(mockReader, times(2)).acquireLatestImage();
551554
}
552555

556+
@Test
557+
public void flutterImageView_detachFromRendererClosesAllImages() {
558+
final ImageReader mockReader = mock(ImageReader.class);
559+
when(mockReader.getMaxImages()).thenReturn(2);
560+
561+
final Image mockImage = mock(Image.class);
562+
when(mockReader.acquireLatestImage()).thenReturn(mockImage);
563+
564+
final FlutterImageView imageView =
565+
spy(
566+
new FlutterImageView(
567+
RuntimeEnvironment.application,
568+
mockReader,
569+
FlutterImageView.SurfaceKind.background));
570+
571+
final FlutterJNI jni = mock(FlutterJNI.class);
572+
imageView.attachToRenderer(new FlutterRenderer(jni));
573+
574+
doNothing().when(imageView).invalidate();
575+
imageView.acquireLatestImage();
576+
imageView.acquireLatestImage();
577+
imageView.detachFromRenderer();
578+
579+
verify(mockImage, times(2)).close();
580+
}
581+
582+
@Test
583+
@SuppressLint("WrongCall") /*View#onDraw*/
584+
public void flutterImageView_onDrawClosesAllImages() {
585+
final ImageReader mockReader = mock(ImageReader.class);
586+
when(mockReader.getMaxImages()).thenReturn(2);
587+
588+
final Image mockImage = mock(Image.class);
589+
when(mockImage.getPlanes()).thenReturn(new Plane[0]);
590+
when(mockReader.acquireLatestImage()).thenReturn(mockImage);
591+
592+
final FlutterImageView imageView =
593+
spy(
594+
new FlutterImageView(
595+
RuntimeEnvironment.application,
596+
mockReader,
597+
FlutterImageView.SurfaceKind.background));
598+
599+
final FlutterJNI jni = mock(FlutterJNI.class);
600+
imageView.attachToRenderer(new FlutterRenderer(jni));
601+
602+
doNothing().when(imageView).invalidate();
603+
imageView.acquireLatestImage();
604+
imageView.acquireLatestImage();
605+
606+
imageView.onDraw(mock(Canvas.class));
607+
imageView.onDraw(mock(Canvas.class));
608+
609+
// 1 image is closed and 1 is active.
610+
verify(mockImage, times(1)).close();
611+
verify(mockReader, times(2)).acquireLatestImage();
612+
613+
// This call doesn't do anything because there isn't
614+
// an image in the queue.
615+
imageView.onDraw(mock(Canvas.class));
616+
verify(mockImage, times(1)).close();
617+
618+
// Aquire another image and push it to the queue.
619+
imageView.acquireLatestImage();
620+
verify(mockReader, times(3)).acquireLatestImage();
621+
622+
// Then, the second image is closed.
623+
imageView.onDraw(mock(Canvas.class));
624+
verify(mockImage, times(2)).close();
625+
}
626+
553627
/*
554628
* A custom shadow that reports fullscreen flag for system UI visibility
555629
*/

0 commit comments

Comments
 (0)