// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugin.platform;

import static android.view.MotionEvent.PointerCoords;
import static android.view.MotionEvent.PointerProperties;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.MutableContextWrapper;
import android.os.Build;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.*;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.util.ViewUtils;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * Manages platform views.
 *
 * <p>Each {@link io.flutter.embedding.engine.FlutterEngine} or {@link
 * io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A platform views
 * controller can be attached to at most one Flutter view.
 */
public class PlatformViewsController implements PlatformViewsAccessibilityDelegate {
  private static final String TAG = "PlatformViewsController";

  // These view types allow out-of-band drawing commands that don't notify the Android view
  // hierarchy.
  // To support these cases, Flutter hosts the embedded view in a VirtualDisplay,
  // and binds the VirtualDisplay to a GL texture that is then composed by the engine.
  // However, there are a few issues with Virtual Displays. For example, they don't fully support
  // accessibility due to https://github.com/flutter/flutter/issues/29717,
  // and keyboard interactions may have non-deterministic behavior.
  // Views that issue out-of-band drawing commands that aren't included in this array are
  // required to call `View#invalidate()` to notify Flutter about the update.
  // This isn't ideal, but given all the other limitations it's a reasonable tradeoff.
  // Related issue: https://github.com/flutter/flutter/issues/103630
  private static Class[] VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY = {SurfaceView.class};

  private final PlatformViewRegistryImpl registry;

  private AndroidTouchProcessor androidTouchProcessor;

  // The context of the Activity or Fragment hosting the render target for the Flutter engine.
  private Context context;

  // The View currently rendering the Flutter UI associated with these platform views.
  private FlutterView flutterView;

  // The texture registry maintaining the textures into which the embedded views will be rendered.
  @Nullable private TextureRegistry textureRegistry;

  @Nullable private TextInputPlugin textInputPlugin;

  // The system channel used to communicate with the framework about platform views.
  private PlatformViewsChannel platformViewsChannel;

  // The accessibility bridge to which accessibility events form the platform views will be
  // dispatched.
  private final AccessibilityEventsDelegate accessibilityEventsDelegate;

  // TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make
  // this private. This is visible as a hack to facilitate testing. This was deemed the least
  // bad option at the time of writing.
  @VisibleForTesting /* package */ final HashMap<Integer, VirtualDisplayController> vdControllers;

  // Maps a virtual display's context to the embedded view hosted in this virtual display.
  // Since each virtual display has it's unique context this allows associating any view with the
  // platform view that
  // it is associated with(e.g if a platform view creates other views in the same virtual display.
  @VisibleForTesting /* package */ final HashMap<Context, View> contextToEmbeddedView;

  // The platform views.
  private final SparseArray<PlatformView> platformViews;

  // The platform view wrappers that are appended to FlutterView.
  //
  // These platform views use a PlatformViewLayer in the framework. This is different than
  // the platform views that use a TextureLayer.
  //
  // This distinction is necessary because a PlatformViewLayer allows to embed Android's
  // SurfaceViews in a Flutter app whereas the texture layer is unable to support such native views.
  //
  // If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't
  // in the view hierarchy.
  //
  // This view provides a wrapper that applies scene builder operations to the platform view.
  // For example, a transform matrix, or setting opacity on the platform view layer.
  private final SparseArray<FlutterMutatorView> platformViewParent;

  // Map of unique IDs to views that render overlay layers.
  private final SparseArray<PlatformOverlayView> overlayLayerViews;

  // The platform view wrappers that are appended to FlutterView.
  //
  // These platform views use a TextureLayer in the framework. This is different than
  // the platform views that use a PlatformViewLayer.
  //
  // This is the default mode, and recommended for better performance.
  private final SparseArray<PlatformViewWrapper> viewWrappers;

  // Next available unique ID for use in overlayLayerViews.
  private int nextOverlayLayerId = 0;

  // Tracks whether the flutterView has been converted to use a FlutterImageView.
  private boolean flutterViewConvertedToImageView = false;

  // When adding platform views using Hybrid Composition, the engine converts the render surface
  // to a FlutterImageView to help improve animation synchronization on Android. This flag allows
  // disabling this conversion through the PlatformView platform channel.
  private boolean synchronizeToNativeViewHierarchy = true;

  // Overlay layer IDs that were displayed since the start of the current frame.
  private final HashSet<Integer> currentFrameUsedOverlayLayerIds;

  // Platform view IDs that were displayed since the start of the current frame.
  private final HashSet<Integer> currentFrameUsedPlatformViewIds;

  // Used to acquire the original motion events using the motionEventIds.
  private final MotionEventTracker motionEventTracker;

  // Whether software rendering is used.
  private boolean usesSoftwareRendering = false;

  private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
      new PlatformViewsChannel.PlatformViewsHandler() {

        @TargetApi(19)
        @Override
        // TODO(egarciad): Remove the need for this.
        // https://github.com/flutter/flutter/issues/96679
        public void createForPlatformViewLayer(
            @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
          // API level 19 is required for `android.graphics.ImageReader`.
          enforceMinimumAndroidApiVersion(19);
          ensureValidRequest(request);

          final PlatformView platformView = createPlatformView(request, false);

          configureForHybridComposition(platformView, request);
          // New code should be added to configureForHybridComposition, not here, unless it is
          // not applicable to fallback from TLHC to HC.
        }

        @TargetApi(20)
        @Override
        public long createForTextureLayer(
            @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
          ensureValidRequest(request);
          final int viewId = request.viewId;
          if (viewWrappers.get(viewId) != null) {
            throw new IllegalStateException(
                "Trying to create an already created platform view, view id: " + viewId);
          }
          if (textureRegistry == null) {
            throw new IllegalStateException(
                "Texture registry is null. This means that platform views controller was detached, view id: "
                    + viewId);
          }
          if (flutterView == null) {
            throw new IllegalStateException(
                "Flutter view is null. This means the platform views controller doesn't have an attached view, view id: "
                    + viewId);
          }

          final PlatformView platformView = createPlatformView(request, true);

          final View embeddedView = platformView.getView();
          if (embeddedView.getParent() != null) {
            throw new IllegalStateException(
                "The Android view returned from PlatformView#getView() was already added to a parent view.");
          }

          // The newer Texture Layer Hybrid Composition mode isn't suppported if any of the
          // following are true:
          // - The embedded view contains any of the VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY view types.
          //   These views allow out-of-band graphics operations that aren't notified to the Android
          //   view hierarchy via callbacks such as ViewParent#onDescendantInvalidated().
          // - The API level is <23, due to TLHC implementation API requirements.
          final boolean supportsTextureLayerMode =
              Build.VERSION.SDK_INT >= 23
                  && !ViewUtils.hasChildViewOfType(
                      embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY);

          // Fall back to Hybrid Composition or Virtual Display when necessary, depending on which
          // fallback mode is requested.
          if (!supportsTextureLayerMode) {
            if (request.displayMode
                == PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode
                    .TEXTURE_WITH_HYBRID_FALLBACK) {
              configureForHybridComposition(platformView, request);
              return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK;
            } else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode.
              return configureForVirtualDisplay(platformView, request);
            }
            // TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change.
            // For now, preserve the 3.0 behavior of falling through to Texture Layer mode even
            // though it won't work correctly.
          }
          return configureForTextureLayerComposition(platformView, request);
        }

        @Override
        public void dispose(int viewId) {
          final PlatformView platformView = platformViews.get(viewId);
          if (platformView == null) {
            Log.e(TAG, "Disposing unknown platform view with id: " + viewId);
            return;
          }
          if (platformView.getView() != null) {
            final View embeddedView = platformView.getView();
            final ViewGroup pvParent = (ViewGroup) embeddedView.getParent();
            if (pvParent != null) {
              // Eagerly remove the embedded view from the PlatformViewWrapper.
              // Without this call, we see some crashes because removing the view
              // is used as a signal to stop processing.
              pvParent.removeView(embeddedView);
            }
          }
          platformViews.remove(viewId);
          try {
            platformView.dispose();
          } catch (RuntimeException exception) {
            Log.e(TAG, "Disposing platform view threw an exception", exception);
          }
          if (usesVirtualDisplay(viewId)) {
            final VirtualDisplayController vdController = vdControllers.get(viewId);
            final View embeddedView = vdController.getView();
            if (embeddedView != null) {
              contextToEmbeddedView.remove(embeddedView.getContext());
            }
            vdController.dispose();
            vdControllers.remove(viewId);
            return;
          }
          // The platform view is displayed using a TextureLayer and is inserted in the view
          // hierarchy.
          final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
          if (viewWrapper != null) {
            viewWrapper.removeAllViews();
            viewWrapper.release();
            viewWrapper.unsetOnDescendantFocusChangeListener();

            final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
            if (wrapperParent != null) {
              wrapperParent.removeView(viewWrapper);
            }
            viewWrappers.remove(viewId);
            return;
          }
          // The platform view is displayed using a PlatformViewLayer.
          final FlutterMutatorView parentView = platformViewParent.get(viewId);
          if (parentView != null) {
            parentView.removeAllViews();
            parentView.unsetOnDescendantFocusChangeListener();

            final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
            if (mutatorViewParent != null) {
              mutatorViewParent.removeView(parentView);
            }
            platformViewParent.remove(viewId);
          }
        }

        @Override
        public void offset(int viewId, double top, double left) {
          if (usesVirtualDisplay(viewId)) {
            // Virtual displays don't need an accessibility offset.
            return;
          }
          // For platform views that use TextureView and are in the view hierarchy, set
          // an offset to the wrapper view.
          // This ensures that the accessibility highlights are drawn in the expected position on
          // screen.
          // This offset doesn't affect the position of the embeded view by itself since the GL
          // texture is positioned by the Flutter engine, which knows where to position different
          // types of layers.
          final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
          if (viewWrapper == null) {
            Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId);
            return;
          }
          final int physicalTop = toPhysicalPixels(top);
          final int physicalLeft = toPhysicalPixels(left);
          final FrameLayout.LayoutParams layoutParams =
              (FrameLayout.LayoutParams) viewWrapper.getLayoutParams();
          layoutParams.topMargin = physicalTop;
          layoutParams.leftMargin = physicalLeft;
          viewWrapper.setLayoutParams(layoutParams);
        }

        @Override
        public void resize(
            @NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
            @NonNull PlatformViewsChannel.PlatformViewBufferResized onComplete) {
          final int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
          final int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
          final int viewId = request.viewId;

          if (usesVirtualDisplay(viewId)) {
            final float originalDisplayDensity = getDisplayDensity();
            final VirtualDisplayController vdController = vdControllers.get(viewId);
            // Resizing involved moving the platform view to a new virtual display. Doing so
            // potentially results in losing an active input connection. To make sure we preserve
            // the input connection when resizing we lock it here and unlock after the resize is
            // complete.
            lockInputConnection(vdController);
            vdController.resize(
                physicalWidth,
                physicalHeight,
                () -> {
                  unlockInputConnection(vdController);
                  // Converting back to logic pixels requires a context, which may no longer be
                  // available. If that happens, assume the same logic/physical relationship as
                  // was present when the request arrived.
                  final float displayDensity =
                      context == null ? originalDisplayDensity : getDisplayDensity();
                  onComplete.run(
                      new PlatformViewsChannel.PlatformViewBufferSize(
                          toLogicalPixels(vdController.getRenderTargetWidth(), displayDensity),
                          toLogicalPixels(vdController.getRenderTargetHeight(), displayDensity)));
                });
            return;
          }

          final PlatformView platformView = platformViews.get(viewId);
          final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
          if (platformView == null || viewWrapper == null) {
            Log.e(TAG, "Resizing unknown platform view with id: " + viewId);
            return;
          }
          // Resize the buffer only when the current buffer size is smaller than the new size.
          // This is required to prevent a situation when smooth keyboard animation
          // resizes the texture too often, such that the GPU and the platform thread don't agree on
          // the timing of the new size.
          // Resizing the texture causes pixel stretching since the size of the GL texture used in
          // the engine is set by the framework, but the texture buffer size is set by the
          // platform down below.
          if (physicalWidth > viewWrapper.getRenderTargetWidth()
              || physicalHeight > viewWrapper.getRenderTargetHeight()) {
            viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
          }

          final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams();
          viewWrapperLayoutParams.width = physicalWidth;
          viewWrapperLayoutParams.height = physicalHeight;
          viewWrapper.setLayoutParams(viewWrapperLayoutParams);

          final View embeddedView = platformView.getView();
          if (embeddedView != null) {
            final ViewGroup.LayoutParams embeddedViewLayoutParams = embeddedView.getLayoutParams();
            embeddedViewLayoutParams.width = physicalWidth;
            embeddedViewLayoutParams.height = physicalHeight;
            embeddedView.setLayoutParams(embeddedViewLayoutParams);
          }
          onComplete.run(
              new PlatformViewsChannel.PlatformViewBufferSize(
                  toLogicalPixels(viewWrapper.getRenderTargetWidth()),
                  toLogicalPixels(viewWrapper.getRenderTargetHeight())));
        }

        @Override
        public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
          final int viewId = touch.viewId;
          final float density = context.getResources().getDisplayMetrics().density;

          if (usesVirtualDisplay(viewId)) {
            final VirtualDisplayController vdController = vdControllers.get(viewId);
            final MotionEvent event = toMotionEvent(density, touch, true);
            vdController.dispatchTouchEvent(event);
            return;
          }

          final PlatformView platformView = platformViews.get(viewId);
          if (platformView == null) {
            Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
            return;
          }
          final View view = platformView.getView();
          if (view == null) {
            Log.e(TAG, "Sending touch to a null view with id: " + viewId);
            return;
          }
          final MotionEvent event = toMotionEvent(density, touch, false);
          view.dispatchTouchEvent(event);
        }

        @TargetApi(17)
        @Override
        public void setDirection(int viewId, int direction) {
          if (!validateDirection(direction)) {
            throw new IllegalStateException(
                "Trying to set unknown direction value: "
                    + direction
                    + "(view id: "
                    + viewId
                    + ")");
          }

          View embeddedView;

          if (usesVirtualDisplay(viewId)) {
            final VirtualDisplayController controller = vdControllers.get(viewId);
            embeddedView = controller.getView();
          } else {
            final PlatformView platformView = platformViews.get(viewId);
            if (platformView == null) {
              Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
              return;
            }
            embeddedView = platformView.getView();
          }
          if (embeddedView == null) {
            Log.e(TAG, "Setting direction to a null view with id: " + viewId);
            return;
          }
          embeddedView.setLayoutDirection(direction);
        }

        @Override
        public void clearFocus(int viewId) {
          View embeddedView;

          if (usesVirtualDisplay(viewId)) {
            final VirtualDisplayController controller = vdControllers.get(viewId);
            embeddedView = controller.getView();
          } else {
            final PlatformView platformView = platformViews.get(viewId);
            if (platformView == null) {
              Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
              return;
            }
            embeddedView = platformView.getView();
          }
          if (embeddedView == null) {
            Log.e(TAG, "Clearing focus on a null view with id: " + viewId);
            return;
          }
          embeddedView.clearFocus();
        }

        @Override
        public void synchronizeToNativeViewHierarchy(boolean yes) {
          synchronizeToNativeViewHierarchy = yes;
        }
      };

  /// Throws an exception if the SDK version is below minSdkVersion.
  private void enforceMinimumAndroidApiVersion(int minSdkVersion) {
    if (Build.VERSION.SDK_INT < minSdkVersion) {
      throw new IllegalStateException(
          "Trying to use platform views with API "
              + Build.VERSION.SDK_INT
              + ", required API level is: "
              + minSdkVersion);
    }
  }

  private void ensureValidRequest(
      @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
    if (!validateDirection(request.direction)) {
      throw new IllegalStateException(
          "Trying to create a view with unknown direction value: "
              + request.direction
              + "(view id: "
              + request.viewId
              + ")");
    }
  }

  // Creates a platform view based on `request`, performs configuration that's common to
  // all display modes, and adds it to `platformViews`.
  @TargetApi(19)
  @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
  public PlatformView createPlatformView(
      @NonNull PlatformViewsChannel.PlatformViewCreationRequest request, boolean wrapContext) {
    final PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
    if (viewFactory == null) {
      throw new IllegalStateException(
          "Trying to create a platform view of unregistered type: " + request.viewType);
    }

    Object createParams = null;
    if (request.params != null) {
      createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
    }

    // In some display modes, the context needs to be modified during display.
    // TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see
    // https://github.com/flutter/flutter/issues/113449
    final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context;
    final PlatformView platformView =
        viewFactory.create(mutableContext, request.viewId, createParams);

    // Configure the view to match the requested layout direction.
    final View embeddedView = platformView.getView();
    if (embeddedView == null) {
      throw new IllegalStateException(
          "PlatformView#getView() returned null, but an Android view reference was expected.");
    }
    embeddedView.setLayoutDirection(request.direction);
    platformViews.put(request.viewId, platformView);
    maybeInvokeOnFlutterViewAttached(platformView);
    return platformView;
  }

  // Configures the view for Hybrid Composition mode.
  private void configureForHybridComposition(
      @NonNull PlatformView platformView,
      @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
    enforceMinimumAndroidApiVersion(19);
    Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId);
  }

  // Configures the view for Virtual Display mode, returning the associated texture ID.
  private long configureForVirtualDisplay(
      @NonNull PlatformView platformView,
      @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
    // This mode adds the view to a virtual display, which is wired up to a GL texture that
    // is composed by the Flutter engine.

    // API level 20 is required to use VirtualDisplay#setSurface.
    enforceMinimumAndroidApiVersion(20);

    Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId);

    final PlatformViewRenderTarget renderTarget = makePlatformViewRenderTarget(textureRegistry);
    final int physicalWidth = toPhysicalPixels(request.logicalWidth);
    final int physicalHeight = toPhysicalPixels(request.logicalHeight);
    final VirtualDisplayController vdController =
        VirtualDisplayController.create(
            context,
            accessibilityEventsDelegate,
            platformView,
            renderTarget,
            physicalWidth,
            physicalHeight,
            request.viewId,
            null,
            (view, hasFocus) -> {
              if (hasFocus) {
                platformViewsChannel.invokeViewFocused(request.viewId);
              }
            });

    if (vdController == null) {
      throw new IllegalStateException(
          "Failed creating virtual display for a "
              + request.viewType
              + " with id: "
              + request.viewId);
    }

    // The embedded view doesn't need to be sized in Virtual Display mode because the
    // virtual display itself is sized.

    vdControllers.put(request.viewId, vdController);
    final View embeddedView = platformView.getView();
    contextToEmbeddedView.put(embeddedView.getContext(), embeddedView);

    return renderTarget.getId();
  }

  // Configures the view for Texture Layer Hybrid Composition mode, returning the associated
  // texture ID.
  @TargetApi(23)
  @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
  public long configureForTextureLayerComposition(
      @NonNull PlatformView platformView,
      @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
    // This mode attaches the view to the Android view hierarchy and record its drawing
    // operations, so they can be forwarded to a GL texture that is composed by the
    // Flutter engine.

    // API level 23 is required to use Surface#lockHardwareCanvas().
    enforceMinimumAndroidApiVersion(23);
    Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId);

    final int physicalWidth = toPhysicalPixels(request.logicalWidth);
    final int physicalHeight = toPhysicalPixels(request.logicalHeight);
    PlatformViewWrapper viewWrapper;
    long textureId;
    if (usesSoftwareRendering) {
      viewWrapper = new PlatformViewWrapper(context);
      textureId = -1;
    } else {
      final PlatformViewRenderTarget renderTarget = makePlatformViewRenderTarget(textureRegistry);
      viewWrapper = new PlatformViewWrapper(context, renderTarget);
      textureId = renderTarget.getId();
    }
    viewWrapper.setTouchProcessor(androidTouchProcessor);
    viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);

    final FrameLayout.LayoutParams viewWrapperLayoutParams =
        new FrameLayout.LayoutParams(physicalWidth, physicalHeight);

    // Size and position the view wrapper.
    final int physicalTop = toPhysicalPixels(request.logicalTop);
    final int physicalLeft = toPhysicalPixels(request.logicalLeft);
    viewWrapperLayoutParams.topMargin = physicalTop;
    viewWrapperLayoutParams.leftMargin = physicalLeft;
    viewWrapper.setLayoutParams(viewWrapperLayoutParams);

    // Size the embedded view.
    final View embeddedView = platformView.getView();
    embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));

    // Accessibility in the embedded view is initially disabled because if a Flutter app
    // disabled accessibility in the first frame, the embedding won't receive an update to
    // disable accessibility since the embedding never received an update to enable it.
    // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas
    // when the framework sends a new a11y tree to the embedding.
    // To prevent races, the framework populate the SemanticsNode after the platform view has
    // been created.
    embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);

    // Add the embedded view to the wrapper.
    viewWrapper.addView(embeddedView);

    // Listen for focus changed in any subview, so the framework is notified when the platform
    // view is focused.
    viewWrapper.setOnDescendantFocusChangeListener(
        (v, hasFocus) -> {
          if (hasFocus) {
            platformViewsChannel.invokeViewFocused(request.viewId);
          } else if (textInputPlugin != null) {
            textInputPlugin.clearPlatformViewClient(request.viewId);
          }
        });

    flutterView.addView(viewWrapper);
    viewWrappers.append(request.viewId, viewWrapper);

    maybeInvokeOnFlutterViewAttached(platformView);

    return textureId;
  }

  @VisibleForTesting
  public MotionEvent toMotionEvent(
      float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) {
    MotionEventTracker.MotionEventId motionEventId =
        MotionEventTracker.MotionEventId.from(touch.motionEventId);
    MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);

    // Pointer coordinates in the tracked events are global to FlutterView
    // framework converts them to be local to a widget, given that
    // motion events operate on local coords, we need to replace these in the tracked
    // event with their local counterparts.
    PointerProperties[] pointerProperties =
        parsePointerPropertiesList(touch.rawPointerPropertiesList)
            .toArray(new PointerProperties[touch.pointerCount]);
    PointerCoords[] pointerCoords =
        parsePointerCoordsList(touch.rawPointerCoords, density)
            .toArray(new PointerCoords[touch.pointerCount]);

    if (!usingVirtualDiplay && trackedEvent != null) {
      return MotionEvent.obtain(
          trackedEvent.getDownTime(),
          trackedEvent.getEventTime(),
          touch.action,
          touch.pointerCount,
          pointerProperties,
          pointerCoords,
          trackedEvent.getMetaState(),
          trackedEvent.getButtonState(),
          trackedEvent.getXPrecision(),
          trackedEvent.getYPrecision(),
          trackedEvent.getDeviceId(),
          trackedEvent.getEdgeFlags(),
          trackedEvent.getSource(),
          trackedEvent.getFlags());
    }

    // TODO (kaushikiska) : warn that we are potentially using an untracked
    // event in the platform views.
    return MotionEvent.obtain(
        touch.downTime.longValue(),
        touch.eventTime.longValue(),
        touch.action,
        touch.pointerCount,
        pointerProperties,
        pointerCoords,
        touch.metaState,
        touch.buttonState,
        touch.xPrecision,
        touch.yPrecision,
        touch.deviceId,
        touch.edgeFlags,
        touch.source,
        touch.flags);
  }

  public PlatformViewsController() {
    registry = new PlatformViewRegistryImpl();
    vdControllers = new HashMap<>();
    accessibilityEventsDelegate = new AccessibilityEventsDelegate();
    contextToEmbeddedView = new HashMap<>();
    overlayLayerViews = new SparseArray<>();
    currentFrameUsedOverlayLayerIds = new HashSet<>();
    currentFrameUsedPlatformViewIds = new HashSet<>();
    viewWrappers = new SparseArray<>();
    platformViews = new SparseArray<>();
    platformViewParent = new SparseArray<>();

    motionEventTracker = MotionEventTracker.getInstance();
  }

  /**
   * Attaches this platform views controller to its input and output channels.
   *
   * @param context The base context that will be passed to embedded views created by this
   *     controller. This should be the context of the Activity hosting the Flutter application.
   * @param textureRegistry The texture registry which provides the output textures into which the
   *     embedded views will be rendered.
   * @param dartExecutor The dart execution context, which is used to set up a system channel.
   */
  public void attach(
      @Nullable Context context,
      @NonNull TextureRegistry textureRegistry,
      @NonNull DartExecutor dartExecutor) {
    if (this.context != null) {
      throw new AssertionError(
          "A PlatformViewsController can only be attached to a single output target.\n"
              + "attach was called while the PlatformViewsController was already attached.");
    }
    this.context = context;
    this.textureRegistry = textureRegistry;
    platformViewsChannel = new PlatformViewsChannel(dartExecutor);
    platformViewsChannel.setPlatformViewsHandler(channelHandler);
  }

  /**
   * Sets whether Flutter uses software rendering.
   *
   * <p>When software rendering is used, no GL context is available on the raster thread. When this
   * is set to true, there's no Flutter composition of Android views and Flutter widgets since GL
   * textures cannot be used.
   *
   * <p>Software rendering is only used for testing in emulators, and it should never be set to true
   * in a production environment.
   *
   * @param useSoftwareRendering Whether software rendering is used.
   */
  public void setSoftwareRendering(boolean useSoftwareRendering) {
    usesSoftwareRendering = useSoftwareRendering;
  }

  /**
   * Detaches this platform views controller.
   *
   * <p>This is typically called when a Flutter applications moves to run in the background, or is
   * destroyed. After calling this the platform views controller will no longer listen to it's
   * previous messenger, and will not maintain references to the texture registry, context, and
   * messenger passed to the previous attach call.
   */
  @UiThread
  public void detach() {
    if (platformViewsChannel != null) {
      platformViewsChannel.setPlatformViewsHandler(null);
    }
    destroyOverlaySurfaces();
    platformViewsChannel = null;
    context = null;
    textureRegistry = null;
  }

  /**
   * Attaches the controller to a {@link FlutterView}.
   *
   * <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
   * after the device rotates since the FlutterView is recreated after a rotation.
   */
  public void attachToView(@NonNull FlutterView newFlutterView) {
    flutterView = newFlutterView;
    // Add wrapper for platform views that use GL texture.
    for (int index = 0; index < viewWrappers.size(); index++) {
      final PlatformViewWrapper view = viewWrappers.valueAt(index);
      flutterView.addView(view);
    }
    // Add wrapper for platform views that are composed at the view hierarchy level.
    for (int index = 0; index < platformViewParent.size(); index++) {
      final FlutterMutatorView view = platformViewParent.valueAt(index);
      flutterView.addView(view);
    }
    // Notify platform views that they are now attached to a FlutterView.
    for (int index = 0; index < platformViews.size(); index++) {
      final PlatformView view = platformViews.valueAt(index);
      view.onFlutterViewAttached(flutterView);
    }
  }

  /**
   * Detaches the controller from {@link FlutterView}.
   *
   * <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
   * when the device rotates since the FlutterView is detached from the fragment. The next time the
   * fragment needs to be displayed, a new Flutter view is created, so attachToView is called again.
   */
  public void detachFromView() {
    // Remove wrapper for platform views that use GL texture.
    for (int index = 0; index < viewWrappers.size(); index++) {
      final PlatformViewWrapper view = viewWrappers.valueAt(index);
      flutterView.removeView(view);
    }
    // Remove wrapper for platform views that are composed at the view hierarchy level.
    for (int index = 0; index < platformViewParent.size(); index++) {
      final FlutterMutatorView view = platformViewParent.valueAt(index);
      flutterView.removeView(view);
    }

    destroyOverlaySurfaces();
    removeOverlaySurfaces();
    flutterView = null;
    flutterViewConvertedToImageView = false;

    // Notify that the platform view have been detached from FlutterView.
    for (int index = 0; index < platformViews.size(); index++) {
      final PlatformView view = platformViews.valueAt(index);
      view.onFlutterViewDetached();
    }
  }

  private void maybeInvokeOnFlutterViewAttached(PlatformView view) {
    if (flutterView == null) {
      Log.i(TAG, "null flutterView");
      // There is currently no FlutterView that we are attached to.
      return;
    }
    view.onFlutterViewAttached(flutterView);
  }

  @Override
  public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) {
    accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
  }

  @Override
  public void detachAccessibilityBridge() {
    accessibilityEventsDelegate.setAccessibilityBridge(null);
  }

  /**
   * Attaches this controller to a text input plugin.
   *
   * <p>While a text input plugin is available, the platform views controller interacts with it to
   * facilitate delegation of text input connections to platform views.
   *
   * <p>A platform views controller should be attached to a text input plugin whenever it is
   * possible for the Flutter framework to receive text input.
   */
  public void attachTextInputPlugin(@NonNull TextInputPlugin textInputPlugin) {
    this.textInputPlugin = textInputPlugin;
  }

  /** Detaches this controller from the currently attached text input plugin. */
  public void detachTextInputPlugin() {
    textInputPlugin = null;
  }

  /**
   * Returns true if Flutter should perform input connection proxying for the view.
   *
   * <p>If the view is a platform view managed by this platform views controller returns true. Else
   * if the view was created in a platform view's VD, delegates the decision to the platform view's
   * {@link View#checkInputConnectionProxy(View)} method. Else returns false.
   */
  public boolean checkInputConnectionProxy(@Nullable View view) {
    // View can be null on some devices
    // See: https://github.com/flutter/flutter/issues/36517
    if (view == null) {
      return false;
    }
    if (!contextToEmbeddedView.containsKey(view.getContext())) {
      return false;
    }
    View platformView = contextToEmbeddedView.get(view.getContext());
    if (platformView == view) {
      return true;
    }
    return platformView.checkInputConnectionProxy(view);
  }

  public PlatformViewRegistry getRegistry() {
    return registry;
  }

  /**
   * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
   * PlatformViewsController} attaches to JNI.
   */
  public void onAttachedToJNI() {
    // Currently no action needs to be taken after JNI attachment.
  }

  /**
   * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
   * PlatformViewsController} detaches from JNI.
   */
  public void onDetachedFromJNI() {
    diposeAllViews();
  }

  public void onPreEngineRestart() {
    diposeAllViews();
  }

  @Override
  @Nullable
  public View getPlatformViewById(int viewId) {
    if (usesVirtualDisplay(viewId)) {
      final VirtualDisplayController controller = vdControllers.get(viewId);
      return controller.getView();
    }

    final PlatformView platformView = platformViews.get(viewId);
    if (platformView == null) {
      return null;
    }
    return platformView.getView();
  }

  @Override
  public boolean usesVirtualDisplay(int id) {
    return vdControllers.containsKey(id);
  }

  private void lockInputConnection(@NonNull VirtualDisplayController controller) {
    if (textInputPlugin == null) {
      return;
    }
    textInputPlugin.lockPlatformViewInputConnection();
    controller.onInputConnectionLocked();
  }

  private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
    if (textInputPlugin == null) {
      return;
    }
    textInputPlugin.unlockPlatformViewInputConnection();
    controller.onInputConnectionUnlocked();
  }

  private static PlatformViewRenderTarget makePlatformViewRenderTarget(
      TextureRegistry textureRegistry) {
    // TODO(johnmccutchan): Enable ImageReaderPlatformViewRenderTarget for public use.
    if (false) {
      final TextureRegistry.ImageTextureEntry textureEntry = textureRegistry.createImageTexture();
      return new ImageReaderPlatformViewRenderTarget(textureEntry);
    }
    final TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
    return new SurfaceTexturePlatformViewRenderTarget(textureEntry);
  }

  private static boolean validateDirection(int direction) {
    return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL;
  }

  @SuppressWarnings("unchecked")
  private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) {
    List<Object> rawProperties = (List<Object>) rawPropertiesList;
    List<PointerProperties> pointerProperties = new ArrayList<>();
    for (Object o : rawProperties) {
      pointerProperties.add(parsePointerProperties(o));
    }
    return pointerProperties;
  }

  @SuppressWarnings("unchecked")
  private static PointerProperties parsePointerProperties(Object rawProperties) {
    List<Object> propertiesList = (List<Object>) rawProperties;
    PointerProperties properties = new MotionEvent.PointerProperties();
    properties.id = (int) propertiesList.get(0);
    properties.toolType = (int) propertiesList.get(1);
    return properties;
  }

  @SuppressWarnings("unchecked")
  private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) {
    List<Object> rawCoords = (List<Object>) rawCoordsList;
    List<PointerCoords> pointerCoords = new ArrayList<>();
    for (Object o : rawCoords) {
      pointerCoords.add(parsePointerCoords(o, density));
    }
    return pointerCoords;
  }

  @SuppressWarnings("unchecked")
  private static PointerCoords parsePointerCoords(Object rawCoords, float density) {
    List<Object> coordsList = (List<Object>) rawCoords;
    PointerCoords coords = new MotionEvent.PointerCoords();
    coords.orientation = (float) (double) coordsList.get(0);
    coords.pressure = (float) (double) coordsList.get(1);
    coords.size = (float) (double) coordsList.get(2);
    coords.toolMajor = (float) (double) coordsList.get(3) * density;
    coords.toolMinor = (float) (double) coordsList.get(4) * density;
    coords.touchMajor = (float) (double) coordsList.get(5) * density;
    coords.touchMinor = (float) (double) coordsList.get(6) * density;
    coords.x = (float) (double) coordsList.get(7) * density;
    coords.y = (float) (double) coordsList.get(8) * density;
    return coords;
  }

  private float getDisplayDensity() {
    return context.getResources().getDisplayMetrics().density;
  }

  private int toPhysicalPixels(double logicalPixels) {
    return (int) Math.round(logicalPixels * getDisplayDensity());
  }

  private int toLogicalPixels(double physicalPixels, float displayDensity) {
    return (int) Math.round(physicalPixels / displayDensity);
  }

  private int toLogicalPixels(double physicalPixels) {
    return toLogicalPixels(physicalPixels, getDisplayDensity());
  }

  private void diposeAllViews() {
    while (platformViews.size() > 0) {
      final int viewId = platformViews.keyAt(0);
      // Dispose deletes the entry from platformViews and clears associated resources.
      channelHandler.dispose(viewId);
    }
  }

  /**
   * Disposes a single
   *
   * @param viewId the PlatformView ID.
   */
  @VisibleForTesting
  public void disposePlatformView(int viewId) {
    channelHandler.dispose(viewId);
  }

  private void initializeRootImageViewIfNeeded() {
    if (synchronizeToNativeViewHierarchy && !flutterViewConvertedToImageView) {
      flutterView.convertToImageView();
      flutterViewConvertedToImageView = true;
    }
  }

  /**
   * Initializes a platform view and adds it to the view hierarchy.
   *
   * @param viewId The view ID. This member is not intended for public use, and is only visible for
   *     testing.
   */
  @VisibleForTesting
  @TargetApi(Build.VERSION_CODES.KITKAT)
  void initializePlatformViewIfNeeded(int viewId) {
    final PlatformView platformView = platformViews.get(viewId);
    if (platformView == null) {
      throw new IllegalStateException(
          "Platform view hasn't been initialized from the platform view channel.");
    }
    if (platformViewParent.get(viewId) != null) {
      return;
    }
    final View embeddedView = platformView.getView();
    if (embeddedView == null) {
      throw new IllegalStateException(
          "PlatformView#getView() returned null, but an Android view reference was expected.");
    }
    if (embeddedView.getParent() != null) {
      throw new IllegalStateException(
          "The Android view returned from PlatformView#getView() was already added to a parent view.");
    }
    final FlutterMutatorView parentView =
        new FlutterMutatorView(
            context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);

    parentView.setOnDescendantFocusChangeListener(
        (view, hasFocus) -> {
          if (hasFocus) {
            platformViewsChannel.invokeViewFocused(viewId);
          } else if (textInputPlugin != null) {
            textInputPlugin.clearPlatformViewClient(viewId);
          }
        });

    platformViewParent.put(viewId, parentView);

    // Accessibility in the embedded view is initially disabled because if a Flutter app disabled
    // accessibility in the first frame, the embedding won't receive an update to disable
    // accessibility since the embedding never received an update to enable it.
    // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas when
    // the framework sends a new a11y tree to the embedding.
    // To prevent races, the framework populate the SemanticsNode after the platform view has been
    // created.
    embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);

    parentView.addView(embeddedView);
    flutterView.addView(parentView);
  }

  public void attachToFlutterRenderer(@NonNull FlutterRenderer flutterRenderer) {
    androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ true);
  }

  /**
   * Called when a platform view id displayed in the current frame.
   *
   * @param viewId The ID of the platform view.
   * @param x The left position relative to {@code FlutterView}.
   * @param y The top position relative to {@code FlutterView}.
   * @param width The width of the platform view.
   * @param height The height of the platform view.
   * @param viewWidth The original width of the platform view before applying the mutator stack.
   * @param viewHeight The original height of the platform view before applying the mutator stack.
   * @param mutatorsStack The mutator stack. This member is not intended for public use, and is only
   *     visible for testing.
   */
  public void onDisplayPlatformView(
      int viewId,
      int x,
      int y,
      int width,
      int height,
      int viewWidth,
      int viewHeight,
      @NonNull FlutterMutatorsStack mutatorsStack) {
    initializeRootImageViewIfNeeded();
    initializePlatformViewIfNeeded(viewId);

    final FlutterMutatorView parentView = platformViewParent.get(viewId);
    parentView.readyToDisplay(mutatorsStack, x, y, width, height);
    parentView.setVisibility(View.VISIBLE);
    parentView.bringToFront();

    final FrameLayout.LayoutParams layoutParams =
        new FrameLayout.LayoutParams(viewWidth, viewHeight);
    final View view = platformViews.get(viewId).getView();
    if (view != null) {
      view.setLayoutParams(layoutParams);
      view.bringToFront();
    }
    currentFrameUsedPlatformViewIds.add(viewId);
  }

  /**
   * Called when an overlay surface is displayed in the current frame.
   *
   * @param id The ID of the surface.
   * @param x The left position relative to {@code FlutterView}.
   * @param y The top position relative to {@code FlutterView}.
   * @param width The width of the surface.
   * @param height The height of the surface. This member is not intended for public use, and is
   *     only visible for testing.
   */
  public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) {
    if (overlayLayerViews.get(id) == null) {
      throw new IllegalStateException("The overlay surface (id:" + id + ") doesn't exist");
    }
    initializeRootImageViewIfNeeded();

    final PlatformOverlayView overlayView = overlayLayerViews.get(id);
    if (overlayView.getParent() == null) {
      flutterView.addView(overlayView);
    }

    FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams((int) width, (int) height);
    layoutParams.leftMargin = (int) x;
    layoutParams.topMargin = (int) y;
    overlayView.setLayoutParams(layoutParams);
    overlayView.setVisibility(View.VISIBLE);
    overlayView.bringToFront();
    currentFrameUsedOverlayLayerIds.add(id);
  }

  public void onBeginFrame() {
    currentFrameUsedOverlayLayerIds.clear();
    currentFrameUsedPlatformViewIds.clear();
  }

  /**
   * Called by {@code FlutterJNI} when the Flutter frame was submitted.
   *
   * <p>This member is not intended for public use, and is only visible for testing.
   */
  public void onEndFrame() {
    // If there are no platform views in the current frame,
    // then revert the image view surface and use the previous surface.
    //
    // Otherwise, acquire the latest image.
    if (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) {
      flutterViewConvertedToImageView = false;
      flutterView.revertImageView(
          () -> {
            // Destroy overlay surfaces once the surface reversion is completed.
            finishFrame(false);
          });
      return;
    }
    // Whether the current frame was rendered using ImageReaders.
    //
    // Since the image readers may not have images available at this point,
    // this becomes true if all the required surfaces have images available.
    //
    // This is used to decide if the platform views can be rendered in the current frame.
    // If one of the surfaces doesn't have an image, the frame may be incomplete and must be
    // dropped.
    // For example, a toolbar widget painted by Flutter may not be rendered.
    final boolean isFrameRenderedUsingImageReaders =
        flutterViewConvertedToImageView && flutterView.acquireLatestImageViewFrame();
    finishFrame(isFrameRenderedUsingImageReaders);
  }

  private void finishFrame(boolean isFrameRenderedUsingImageReaders) {
    for (int i = 0; i < overlayLayerViews.size(); i++) {
      final int overlayId = overlayLayerViews.keyAt(i);
      final PlatformOverlayView overlayView = overlayLayerViews.valueAt(i);

      if (currentFrameUsedOverlayLayerIds.contains(overlayId)) {
        flutterView.attachOverlaySurfaceToRender(overlayView);
        final boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage();
        isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage;
      } else {
        // If the background surface isn't rendered by the image view, then the
        // overlay surfaces can be detached from the rendered.
        // This releases resources used by the ImageReader.
        if (!flutterViewConvertedToImageView) {
          overlayView.detachFromRenderer();
        }
        // Hide overlay surfaces that aren't rendered in the current frame.
        overlayView.setVisibility(View.GONE);
        flutterView.removeView(overlayView);
      }
    }

    for (int i = 0; i < platformViewParent.size(); i++) {
      final int viewId = platformViewParent.keyAt(i);
      final View parentView = platformViewParent.get(viewId);

      // This should only show platform views that are rendered in this frame and either:
      //  1. Surface has images available in this frame or,
      //  2. Surface does not have images available in this frame because the render surface should
      // not be an ImageView.
      //
      // The platform view is appended to a mutator view.
      //
      // Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as
      // they are removed when the framework disposes the platform view widget.
      if (currentFrameUsedPlatformViewIds.contains(viewId)
          && (isFrameRenderedUsingImageReaders || !synchronizeToNativeViewHierarchy)) {
        parentView.setVisibility(View.VISIBLE);
      } else {
        parentView.setVisibility(View.GONE);
      }
    }
  }

  /**
   * Creates and tracks the overlay surface.
   *
   * @param imageView The surface that displays the overlay.
   * @return Wrapper object that provides the layer id and the surface. This member is not intended
   *     for public use, and is only visible for testing.
   */
  @VisibleForTesting
  @TargetApi(19)
  @NonNull
  public FlutterOverlaySurface createOverlaySurface(@NonNull PlatformOverlayView imageView) {
    final int id = nextOverlayLayerId++;
    overlayLayerViews.put(id, imageView);
    return new FlutterOverlaySurface(id, imageView.getSurface());
  }

  /**
   * Creates an overlay surface while the Flutter view is rendered by {@code PlatformOverlayView}.
   *
   * <p>This method is invoked by {@code FlutterJNI} only.
   *
   * <p>This member is not intended for public use, and is only visible for testing.
   */
  @TargetApi(19)
  @NonNull
  public FlutterOverlaySurface createOverlaySurface() {
    // Overlay surfaces have the same size as the background surface.
    //
    // This allows to reuse these surfaces in consecutive frames even
    // if the drawings they contain have a different tight bound.
    //
    // The final view size is determined when its frame is set.
    return createOverlaySurface(
        new PlatformOverlayView(
            flutterView.getContext(),
            flutterView.getWidth(),
            flutterView.getHeight(),
            accessibilityEventsDelegate));
  }

  /**
   * Destroys the overlay surfaces and removes them from the view hierarchy.
   *
   * <p>This method is used only internally by {@code FlutterJNI}.
   */
  public void destroyOverlaySurfaces() {
    for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) {
      final PlatformOverlayView overlayView = overlayLayerViews.valueAt(viewId);
      overlayView.detachFromRenderer();
      overlayView.closeImageReader();
      // Don't remove overlayView from the view hierarchy since this method can
      // be called while the Android framework is iterating over the array of views.
      // See ViewGroup#dispatchDetachedFromWindow(), and
      // https://github.com/flutter/flutter/issues/97679.
    }
  }

  private void removeOverlaySurfaces() {
    if (flutterView == null) {
      Log.e(TAG, "removeOverlaySurfaces called while flutter view is null");
      return;
    }
    for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) {
      flutterView.removeView(overlayLayerViews.valueAt(viewId));
    }
    overlayLayerViews.clear();
  }

  @VisibleForTesting
  public SparseArray<PlatformOverlayView> getOverlayLayerViews() {
    return overlayLayerViews;
  }
}
