Basic support for resizing overlay surfaces in hybrid composition (#19402)

diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc
index 8bb4eaa..259deec7 100644
--- a/shell/platform/android/external_view_embedder/external_view_embedder.cc
+++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc
@@ -238,6 +238,9 @@
     double device_pixel_ratio,
     fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
   Reset();
+  if (frame_size_ != frame_size) {
+    surface_pool_->DestroyLayers(jni_facade_);
+  }
   frame_size_ = frame_size;
   device_pixel_ratio_ = device_pixel_ratio;
   // JNI method must be called on the platform thread.
diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc
index 8bac045..b7d329c 100644
--- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc
+++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc
@@ -402,5 +402,20 @@
   embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger);
 }
 
+TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) {
+  auto jni_mock = std::make_shared<JNIMock>();
+  auto embedder =
+      std::make_unique<AndroidExternalViewEmbedder>(nullptr, jni_mock, nullptr);
+
+  auto raster_thread_merger = GetThreadMergerFromPlatformThread();
+  ASSERT_FALSE(raster_thread_merger->IsMerged());
+
+  embedder->BeginFrame(SkISize::Make(10, 20), nullptr, 1.0,
+                       raster_thread_merger);
+  EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces());
+  embedder->BeginFrame(SkISize::Make(30, 40), nullptr, 1.0,
+                       raster_thread_merger);
+}
+
 }  // namespace testing
 }  // namespace flutter
diff --git a/shell/platform/android/external_view_embedder/surface_pool.cc b/shell/platform/android/external_view_embedder/surface_pool.cc
index 8258234..934b153 100644
--- a/shell/platform/android/external_view_embedder/surface_pool.cc
+++ b/shell/platform/android/external_view_embedder/surface_pool.cc
@@ -70,6 +70,13 @@
   available_layer_index_ = 0;
 }
 
+void SurfacePool::DestroyLayers(
+    std::shared_ptr<PlatformViewAndroidJNI> jni_facade) {
+  layers_.clear();
+  available_layer_index_ = 0;
+  jni_facade->FlutterViewDestroyOverlaySurfaces();
+}
+
 std::vector<std::shared_ptr<OverlayLayer>> SurfacePool::GetUnusedLayers() {
   std::vector<std::shared_ptr<OverlayLayer>> results;
   for (size_t i = available_layer_index_; i < layers_.size(); i++) {
diff --git a/shell/platform/android/external_view_embedder/surface_pool.h b/shell/platform/android/external_view_embedder/surface_pool.h
index e4eee52..f4412a8 100644
--- a/shell/platform/android/external_view_embedder/surface_pool.h
+++ b/shell/platform/android/external_view_embedder/surface_pool.h
@@ -64,6 +64,9 @@
   // Marks the layers in the pool as available for reuse.
   void RecycleLayers();
 
+  // Destroys all the layers in the pool.
+  void DestroyLayers(std::shared_ptr<PlatformViewAndroidJNI> jni_facade);
+
  private:
   // The index of the entry in the layers_ vector that determines the beginning
   // of the unused layers. For example, consider the following vector:
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
index f4f1677..dfc5fae 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
@@ -10,13 +10,16 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.ColorSpace;
+import android.graphics.PixelFormat;
 import android.hardware.HardwareBuffer;
 import android.media.Image;
 import android.media.Image.Plane;
 import android.media.ImageReader;
+import android.view.Surface;
 import android.view.View;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import io.flutter.embedding.engine.renderer.FlutterRenderer;
 import io.flutter.embedding.engine.renderer.RenderSurface;
 
@@ -35,7 +38,7 @@
 @SuppressLint("ViewConstructor")
 @TargetApi(19)
 public class FlutterImageView extends View implements RenderSurface {
-  private final ImageReader imageReader;
+  @NonNull private ImageReader imageReader;
   @Nullable private Image nextImage;
   @Nullable private Image currentImage;
   @Nullable private Bitmap currentBitmap;
@@ -63,13 +66,39 @@
    * Constructs a {@code FlutterImageView} with an {@link android.media.ImageReader} that provides
    * the Flutter UI.
    */
-  public FlutterImageView(
-      @NonNull Context context, @NonNull ImageReader imageReader, SurfaceKind kind) {
+  public FlutterImageView(@NonNull Context context, int width, int height, SurfaceKind kind) {
+    super(context, null);
+    this.imageReader = createImageReader(width, height);
+    this.kind = kind;
+  }
+
+  @VisibleForTesting
+  FlutterImageView(@NonNull Context context, @NonNull ImageReader imageReader, SurfaceKind kind) {
     super(context, null);
     this.imageReader = imageReader;
     this.kind = kind;
   }
 
+  @TargetApi(19)
+  @NonNull
+  private static ImageReader createImageReader(int width, int height) {
+    if (android.os.Build.VERSION.SDK_INT >= 29) {
+      return ImageReader.newInstance(
+          width,
+          height,
+          PixelFormat.RGBA_8888,
+          3,
+          HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
+    } else {
+      return ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3);
+    }
+  }
+
+  @NonNull
+  public Surface getSurface() {
+    return imageReader.getSurface();
+  }
+
   @Nullable
   @Override
   public FlutterRenderer getAttachedRenderer() {
@@ -179,4 +208,15 @@
       currentBitmap.copyPixelsFromBuffer(imagePlane.getBuffer());
     }
   }
+
+  @Override
+  protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+    if (width == imageReader.getWidth() && height == imageReader.getHeight()) {
+      return;
+    }
+    if (kind == SurfaceKind.background && flutterRenderer != null) {
+      imageReader = createImageReader(width, height);
+      flutterRenderer.swapSurface(imageReader.getSurface());
+    }
+  }
 }
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index 8e4636d..465f718 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -10,7 +10,6 @@
 import android.content.res.Configuration;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.media.ImageReader;
 import android.os.Build;
 import android.text.format.DateFormat;
 import android.util.AttributeSet;
@@ -945,9 +944,9 @@
   public void convertToImageView() {
     renderSurface.pause();
 
-    ImageReader imageReader = PlatformViewsController.createImageReader(getWidth(), getHeight());
     flutterImageView =
-        new FlutterImageView(getContext(), imageReader, FlutterImageView.SurfaceKind.background);
+        new FlutterImageView(
+            getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background);
     renderSurface = flutterImageView;
     if (flutterEngine != null) {
       renderSurface.attachToRenderer(flutterEngine.getRenderer());
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
index 9c24f06..f344df6 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
@@ -855,6 +855,17 @@
     }
     return platformViewsController.createOverlaySurface();
   }
+
+  @SuppressWarnings("unused")
+  @UiThread
+  public void destroyOverlaySurfaces() {
+    ensureRunningOnMainThread();
+    if (platformViewsController == null) {
+      throw new RuntimeException(
+          "platformViewsController must be set before attempting to destroy an overlay surface");
+    }
+    platformViewsController.destroyOverlaySurfaces();
+  }
   // ----- End Engine Lifecycle Support ----
 
   // ----- Start Localization Support ----
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index f776357..40c56be 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -9,9 +9,6 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.graphics.PixelFormat;
-import android.hardware.HardwareBuffer;
-import android.media.ImageReader;
 import android.os.Build;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -694,28 +691,20 @@
   }
 
   @TargetApi(19)
-  public static ImageReader createImageReader(int width, int height) {
-    if (android.os.Build.VERSION.SDK_INT >= 29) {
-      return ImageReader.newInstance(
-          width,
-          height,
-          PixelFormat.RGBA_8888,
-          3,
-          HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
-    } else {
-      return ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3);
-    }
-  }
-
-  @TargetApi(19)
   public FlutterOverlaySurface createOverlaySurface() {
-    ImageReader imageReader = createImageReader(flutterView.getWidth(), flutterView.getHeight());
     FlutterImageView imageView =
         new FlutterImageView(
-            flutterView.getContext(), imageReader, FlutterImageView.SurfaceKind.overlay);
+            flutterView.getContext(),
+            flutterView.getWidth(),
+            flutterView.getHeight(),
+            FlutterImageView.SurfaceKind.overlay);
     int id = nextOverlayLayerId++;
     overlayLayerViews.put(id, imageView);
 
-    return new FlutterOverlaySurface(id, imageReader.getSurface());
+    return new FlutterOverlaySurface(id, imageView.getSurface());
+  }
+
+  public void destroyOverlaySurfaces() {
+    overlayLayerViews.clear();
   }
 }
diff --git a/shell/platform/android/jni/jni_mock.h b/shell/platform/android/jni/jni_mock.h
index 245c8e7..280e421 100644
--- a/shell/platform/android/jni/jni_mock.h
+++ b/shell/platform/android/jni/jni_mock.h
@@ -80,6 +80,8 @@
               (),
               (override));
 
+  MOCK_METHOD(void, FlutterViewDestroyOverlaySurfaces, (), (override));
+
   MOCK_METHOD(std::unique_ptr<std::vector<std::string>>,
               FlutterViewComputePlatformResolvedLocale,
               (std::vector<std::string> supported_locales_data),
diff --git a/shell/platform/android/jni/platform_view_android_jni.h b/shell/platform/android/jni/platform_view_android_jni.h
index 311209d..9d6e9b1 100644
--- a/shell/platform/android/jni/platform_view_android_jni.h
+++ b/shell/platform/android/jni/platform_view_android_jni.h
@@ -176,6 +176,13 @@
   FlutterViewCreateOverlaySurface() = 0;
 
   //----------------------------------------------------------------------------
+  /// @brief      Destroys the overlay surfaces.
+  ///
+  /// @note       Must be called from the platform thread.
+  ///
+  virtual void FlutterViewDestroyOverlaySurfaces() = 0;
+
+  //----------------------------------------------------------------------------
   /// @brief      Computes the locale Android would select.
   ///
   virtual std::unique_ptr<std::vector<std::string>>
diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc
index 2675282..e95b47d 100644
--- a/shell/platform/android/platform_view_android_jni_impl.cc
+++ b/shell/platform/android/platform_view_android_jni_impl.cc
@@ -83,6 +83,8 @@
 
 static jmethodID g_create_overlay_surface_method = nullptr;
 
+static jmethodID g_destroy_overlay_surfaces_method = nullptr;
+
 static jmethodID g_on_begin_frame_method = nullptr;
 
 static jmethodID g_on_end_frame_method = nullptr;
@@ -721,6 +723,14 @@
     return false;
   }
 
+  g_destroy_overlay_surfaces_method = env->GetMethodID(
+      g_flutter_jni_class->obj(), "destroyOverlaySurfaces", "()V");
+
+  if (g_destroy_overlay_surfaces_method == nullptr) {
+    FML_LOG(ERROR) << "Could not locate destroyOverlaySurfaces method";
+    return false;
+  }
+
   fml::jni::ScopedJavaLocalRef<jclass> overlay_surface_class(
       env, env->FindClass("io/flutter/embedding/engine/FlutterOverlaySurface"));
   if (overlay_surface_class.is_null()) {
@@ -1179,6 +1189,19 @@
       overlay_id, std::move(overlay_window));
 }
 
+void PlatformViewAndroidJNIImpl::FlutterViewDestroyOverlaySurfaces() {
+  JNIEnv* env = fml::jni::AttachCurrentThread();
+
+  auto java_object = java_object_.get(env);
+  if (java_object.is_null()) {
+    return;
+  }
+
+  env->CallVoidMethod(java_object.obj(), g_destroy_overlay_surfaces_method);
+
+  FML_CHECK(CheckException(env));
+}
+
 std::unique_ptr<std::vector<std::string>>
 PlatformViewAndroidJNIImpl::FlutterViewComputePlatformResolvedLocale(
     std::vector<std::string> supported_locales_data) {
diff --git a/shell/platform/android/platform_view_android_jni_impl.h b/shell/platform/android/platform_view_android_jni_impl.h
index b496f2d..cf743bb 100644
--- a/shell/platform/android/platform_view_android_jni_impl.h
+++ b/shell/platform/android/platform_view_android_jni_impl.h
@@ -69,6 +69,8 @@
   std::unique_ptr<PlatformViewAndroidJNI::OverlayMetadata>
   FlutterViewCreateOverlaySurface() override;
 
+  void FlutterViewDestroyOverlaySurfaces() override;
+
   std::unique_ptr<std::vector<std::string>>
   FlutterViewComputePlatformResolvedLocale(
       std::vector<std::string> supported_locales_data) override;