// 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.embedding.engine;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.ImageDecoder;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Looper;
import android.util.Size;
import android.view.Surface;
import android.view.SurfaceHolder;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener;
import io.flutter.embedding.engine.dart.PlatformMessageHandler;
import io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager;
import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.util.Preconditions;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.FlutterCallbackInformation;
import io.flutter.view.TextureRegistry;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Interface between Flutter embedding's Java code and Flutter engine's C/C++ code.
 *
 * <p>Flutter's engine is built with C/C++. The Android Flutter embedding is responsible for
 * coordinating Android OS events and app user interactions with the C/C++ engine. Such coordination
 * requires messaging from an Android app in Java code to the C/C++ engine code. This communication
 * requires a JNI (Java Native Interface) API to cross the Java/native boundary.
 *
 * <p>The entirety of Flutter's JNI API is codified in {@code FlutterJNI}. There are multiple
 * reasons that all such calls are centralized in one class. First, JNI calls are inherently static
 * and contain no Java implementation, therefore there is little reason to associate calls with
 * different classes. Second, every JNI call must be registered in C/C++ code and this registration
 * becomes more complicated with every additional Java class that contains JNI calls. Third, most
 * Android developers are not familiar with native development or JNI intricacies, therefore it is
 * in the interest of future maintenance to reduce the API surface that includes JNI declarations.
 * Thus, all Flutter JNI calls are centralized in {@code FlutterJNI}.
 *
 * <p>Despite the fact that individual JNI calls are inherently static, there is state that exists
 * within {@code FlutterJNI}. Most calls within {@code FlutterJNI} correspond to a specific
 * "platform view", of which there may be many. Therefore, each {@code FlutterJNI} instance holds
 * onto a "native platform view ID" after {@link #attachToNative()}, which is shared with the native
 * C/C++ engine code. That ID is passed to every platform-view-specific native method. ID management
 * is handled within {@code FlutterJNI} so that developers don't have to hold onto that ID.
 *
 * <p>To connect part of an Android app to Flutter's C/C++ engine, instantiate a {@code FlutterJNI}
 * and then attach it to the native side:
 *
 * <pre>{@code
 * // Instantiate FlutterJNI and attach to the native side.
 * FlutterJNI flutterJNI = new FlutterJNI();
 * flutterJNI.attachToNative();
 *
 * // Use FlutterJNI as desired. flutterJNI.dispatchPointerDataPacket(...);
 *
 * // Destroy the connection to the native side and cleanup.
 * flutterJNI.detachFromNativeAndReleaseResources();
 * }</pre>
 *
 * <p>To receive callbacks for certain events that occur on the native side, register listeners:
 *
 * <ol>
 *   <li>{@link #addEngineLifecycleListener(FlutterEngine.EngineLifecycleListener)}
 *   <li>{@link #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}
 * </ol>
 *
 * To facilitate platform messages between Java and Dart running in Flutter, register a handler:
 *
 * <p>{@link #setPlatformMessageHandler(PlatformMessageHandler)}
 *
 * <p>To invoke a native method that is not associated with a platform view, invoke it statically:
 *
 * <p>{@code bool enabled = FlutterJNI.getIsSoftwareRenderingEnabled(); }
 */
@Keep
public class FlutterJNI {
  private static final String TAG = "FlutterJNI";
  // This serializes the invocation of platform message responses and the
  // attachment and detachment of the shell holder.  This ensures that we don't
  // detach FlutterJNI on the platform thread while a background thread invokes
  // a message response.  Typically accessing the shell holder happens on the
  // platform thread and doesn't require locking.
  private ReentrantReadWriteLock shellHolderLock = new ReentrantReadWriteLock();

  // Prefer using the FlutterJNI.Factory so it's easier to test.
  public FlutterJNI() {
    // We cache the main looper so that we can ensure calls are made on the main thread
    // without consistently paying the synchronization cost of getMainLooper().
    mainLooper = Looper.getMainLooper();
  }

  /**
   * A factory for creating {@code FlutterJNI} instances. Useful for FlutterJNI injections during
   * tests.
   */
  public static class Factory {
    /** @return a {@link FlutterJNI} instance. */
    public FlutterJNI provideFlutterJNI() {
      return new FlutterJNI();
    }
  }

  // BEGIN Methods related to loading for FlutterLoader.
  /**
   * Loads the libflutter.so C++ library.
   *
   * <p>This must be called before any other native methods, and can be overridden by tests to avoid
   * loading native libraries.
   *
   * <p>This method should only be called once across all FlutterJNI instances.
   */
  public void loadLibrary() {
    if (FlutterJNI.loadLibraryCalled) {
      Log.w(TAG, "FlutterJNI.loadLibrary called more than once");
    }

    System.loadLibrary("flutter");
    FlutterJNI.loadLibraryCalled = true;
  }

  private static boolean loadLibraryCalled = false;

  private static native void nativePrefetchDefaultFontManager();

  /**
   * Prefetch the default font manager provided by SkFontMgr::RefDefault() which is a process-wide
   * singleton owned by Skia. Note that, the first call to SkFontMgr::RefDefault() will take
   * noticeable time, but later calls will return a reference to the preexisting font manager.
   *
   * <p>This method should only be called once across all FlutterJNI instances.
   */
  public void prefetchDefaultFontManager() {
    if (FlutterJNI.prefetchDefaultFontManagerCalled) {
      Log.w(TAG, "FlutterJNI.prefetchDefaultFontManager called more than once");
    }

    FlutterJNI.nativePrefetchDefaultFontManager();
    FlutterJNI.prefetchDefaultFontManagerCalled = true;
  }

  private static boolean prefetchDefaultFontManagerCalled = false;

  private static native void nativeInit(
      @NonNull Context context,
      @NonNull String[] args,
      @Nullable String bundlePath,
      @NonNull String appStoragePath,
      @NonNull String engineCachesPath,
      long initTimeMillis);

  /**
   * Perform one time initialization of the Dart VM and Flutter engine.
   *
   * <p>This method must be called only once. Calling more than once will cause an exception.
   *
   * @param context The application context.
   * @param args Arguments to the Dart VM/Flutter engine.
   * @param bundlePath For JIT runtimes, the path to the Dart kernel file for the application.
   * @param appStoragePath The path to the application data directory.
   * @param engineCachesPath The path to the application cache directory.
   * @param initTimeMillis The time, in milliseconds, taken for initialization.
   */
  public void init(
      @NonNull Context context,
      @NonNull String[] args,
      @Nullable String bundlePath,
      @NonNull String appStoragePath,
      @NonNull String engineCachesPath,
      long initTimeMillis) {
    if (FlutterJNI.initCalled) {
      Log.w(TAG, "FlutterJNI.init called more than once");
    }

    FlutterJNI.nativeInit(
        context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis);
    FlutterJNI.initCalled = true;
  }

  private static boolean initCalled = false;
  // END methods related to FlutterLoader

  @Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate;

  /**
   * This value is updated by the VsyncWaiter when it is initialized.
   *
   * <p>On API 17+, it is updated whenever the default display refresh rate changes.
   *
   * <p>It is defaulted to 60.
   */
  private static float refreshRateFPS = 60.0f;

  private static float displayWidth = -1.0f;
  private static float displayHeight = -1.0f;
  private static float displayDensity = -1.0f;

  // This is set from native code via JNI.
  @Nullable private static String vmServiceUri;

  private native boolean nativeGetIsSoftwareRenderingEnabled();

  /**
   * Checks launch settings for whether software rendering is requested.
   *
   * <p>The value is the same per program.
   */
  @UiThread
  public boolean getIsSoftwareRenderingEnabled() {
    return nativeGetIsSoftwareRenderingEnabled();
  }

  /**
   * VM Service URI for the VM instance.
   *
   * <p>Its value is set by the native engine once {@link #init(Context, String[], String, String,
   * String, long)} is run.
   */
  @Nullable
  public static String getVMServiceUri() {
    return vmServiceUri;
  }

  /**
   * VM Service URI for the VM instance.
   *
   * <p>Its value is set by the native engine once {@link #init(Context, String[], String, String,
   * String, long)} is run.
   *
   * @deprecated replaced by {@link #getVMServiceUri()}.
   */
  @Deprecated
  @Nullable
  public static String getObservatoryUri() {
    return vmServiceUri;
  }

  /**
   * Notifies the engine about the refresh rate of the display when the API level is below 30.
   *
   * <p>For API 30 and above, this value is ignored.
   *
   * <p>Calling this method multiple times will update the refresh rate for the next vsync period.
   * However, callers should avoid calling {@link android.view.Display#getRefreshRate} frequently,
   * since it is expensive on some vendor implementations.
   *
   * @param refreshRateFPS The refresh rate in nanoseconds.
   */
  public void setRefreshRateFPS(float refreshRateFPS) {
    // This is ok because it only ever tracks the refresh rate of the main
    // display. If we ever need to support the refresh rate of other displays
    // on Android we will need to refactor this. Static lookup makes things a
    // bit easier on the C++ side.
    FlutterJNI.refreshRateFPS = refreshRateFPS;
    updateRefreshRate();
  }

  public void updateDisplayMetrics(int displayId, float width, float height, float density) {
    FlutterJNI.displayWidth = width;
    FlutterJNI.displayHeight = height;
    FlutterJNI.displayDensity = density;
    if (!FlutterJNI.loadLibraryCalled) {
      return;
    }
    nativeUpdateDisplayMetrics(nativeShellHolderId);
  }

  private native void nativeUpdateDisplayMetrics(long nativeShellHolderId);

  public void updateRefreshRate() {
    if (!FlutterJNI.loadLibraryCalled) {
      return;
    }
    nativeUpdateRefreshRate(refreshRateFPS);
  }

  private native void nativeUpdateRefreshRate(float refreshRateFPS);

  /**
   * The Android vsync waiter implementation in C++ needs to know when a vsync signal arrives, which
   * is obtained via Java API. The delegate set here is called on the C++ side when the engine is
   * ready to wait for the next vsync signal. The delegate is expected to add a postFrameCallback to
   * the {@link android.view.Choreographer}, and call {@link onVsync} to notify the engine.
   *
   * @param delegate The delegate that will call the engine back on the next vsync signal.
   */
  public void setAsyncWaitForVsyncDelegate(@Nullable AsyncWaitForVsyncDelegate delegate) {
    asyncWaitForVsyncDelegate = delegate;
  }

  // TODO(mattcarroll): add javadocs
  // Called by native.
  private static void asyncWaitForVsync(final long cookie) {
    if (asyncWaitForVsyncDelegate != null) {
      asyncWaitForVsyncDelegate.asyncWaitForVsync(cookie);
    } else {
      throw new IllegalStateException(
          "An AsyncWaitForVsyncDelegate must be registered with FlutterJNI before asyncWaitForVsync() is invoked.");
    }
  }

  private native void nativeOnVsync(long frameDelayNanos, long refreshPeriodNanos, long cookie);

  /**
   * Notifies the engine that the Choreographer has signaled a vsync.
   *
   * @param frameDelayNanos The time in nanoseconds when the frame started being rendered,
   *     subtracted from the {@link System#nanoTime} timebase.
   * @param refreshPeriodNanos The display refresh period in nanoseconds.
   * @param cookie An opaque handle to the C++ VSyncWaiter object.
   */
  public void onVsync(long frameDelayNanos, long refreshPeriodNanos, long cookie) {
    nativeOnVsync(frameDelayNanos, refreshPeriodNanos, cookie);
  }

  @NonNull
  @Deprecated
  public static native FlutterCallbackInformation nativeLookupCallbackInformation(long handle);

  // ----- Start FlutterTextUtils Methods ----
  private native boolean nativeFlutterTextUtilsIsEmoji(int codePoint);

  public boolean isCodePointEmoji(int codePoint) {
    return nativeFlutterTextUtilsIsEmoji(codePoint);
  }

  private native boolean nativeFlutterTextUtilsIsEmojiModifier(int codePoint);

  public boolean isCodePointEmojiModifier(int codePoint) {
    return nativeFlutterTextUtilsIsEmojiModifier(codePoint);
  }

  private native boolean nativeFlutterTextUtilsIsEmojiModifierBase(int codePoint);

  public boolean isCodePointEmojiModifierBase(int codePoint) {
    return nativeFlutterTextUtilsIsEmojiModifierBase(codePoint);
  }

  private native boolean nativeFlutterTextUtilsIsVariationSelector(int codePoint);

  public boolean isCodePointVariantSelector(int codePoint) {
    return nativeFlutterTextUtilsIsVariationSelector(codePoint);
  }

  private native boolean nativeFlutterTextUtilsIsRegionalIndicator(int codePoint);

  public boolean isCodePointRegionalIndicator(int codePoint) {
    return nativeFlutterTextUtilsIsRegionalIndicator(codePoint);
  }

  // ----- End Engine FlutterTextUtils Methods ----

  // Below represents the stateful part of the FlutterJNI instances that aren't static per program.
  // Conceptually, it represents a native shell instance.

  @Nullable private Long nativeShellHolderId;
  @Nullable private AccessibilityDelegate accessibilityDelegate;
  @Nullable private PlatformMessageHandler platformMessageHandler;
  @Nullable private LocalizationPlugin localizationPlugin;
  @Nullable private PlatformViewsController platformViewsController;

  @Nullable private DeferredComponentManager deferredComponentManager;

  @NonNull
  private final Set<EngineLifecycleListener> engineLifecycleListeners = new CopyOnWriteArraySet<>();

  @NonNull
  private final Set<FlutterUiDisplayListener> flutterUiDisplayListeners =
      new CopyOnWriteArraySet<>();

  @NonNull private final Looper mainLooper; // cached to avoid synchronization on repeat access.

  // ------ Start Native Attach/Detach Support ----
  /**
   * Returns true if this instance of {@code FlutterJNI} is connected to Flutter's native engine via
   * a Java Native Interface (JNI).
   */
  public boolean isAttached() {
    return nativeShellHolderId != null;
  }

  /**
   * Attaches this {@code FlutterJNI} instance to Flutter's native engine, which allows for
   * communication between Android code and Flutter's platform agnostic engine.
   *
   * <p>This method must not be invoked if {@code FlutterJNI} is already attached to native.
   */
  @UiThread
  public void attachToNative() {
    ensureRunningOnMainThread();
    ensureNotAttachedToNative();
    shellHolderLock.writeLock().lock();
    try {
      nativeShellHolderId = performNativeAttach(this);
    } finally {
      shellHolderLock.writeLock().unlock();
    }
  }

  @VisibleForTesting
  public long performNativeAttach(@NonNull FlutterJNI flutterJNI) {
    return nativeAttach(flutterJNI);
  }

  private native long nativeAttach(@NonNull FlutterJNI flutterJNI);

  /**
   * Spawns a new FlutterJNI instance from the current instance.
   *
   * <p>This creates another native shell from the current shell. This causes the 2 shells to re-use
   * some of the shared resources, reducing the total memory consumption versus creating a new
   * FlutterJNI by calling its standard constructor.
   *
   * <p>This can only be called once the current FlutterJNI instance is attached by calling {@link
   * #attachToNative()}.
   *
   * <p>Static methods that should be only called once such as {@link #init(Context, String[],
   * String, String, String, long)} shouldn't be called again on the spawned FlutterJNI instance.
   */
  @UiThread
  @NonNull
  public FlutterJNI spawn(
      @Nullable String entrypointFunctionName,
      @Nullable String pathToEntrypointFunction,
      @Nullable String initialRoute,
      @Nullable List<String> entrypointArgs) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    FlutterJNI spawnedJNI =
        nativeSpawn(
            nativeShellHolderId,
            entrypointFunctionName,
            pathToEntrypointFunction,
            initialRoute,
            entrypointArgs);
    Preconditions.checkState(
        spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId != 0,
        "Failed to spawn new JNI connected shell from existing shell.");

    return spawnedJNI;
  }

  private native FlutterJNI nativeSpawn(
      long nativeSpawningShellId,
      @Nullable String entrypointFunctionName,
      @Nullable String pathToEntrypointFunction,
      @Nullable String initialRoute,
      @Nullable List<String> entrypointArgs);

  /**
   * Detaches this {@code FlutterJNI} instance from Flutter's native engine, which precludes any
   * further communication between Android code and Flutter's platform agnostic engine.
   *
   * <p>This method must not be invoked if {@code FlutterJNI} is not already attached to native.
   *
   * <p>Invoking this method will result in the release of all native-side resources that were set
   * up during {@link #attachToNative()} or {@link #spawn(String, String, String, List)}, or
   * accumulated thereafter.
   *
   * <p>It is permissible to re-attach this instance to native after detaching it from native.
   */
  @UiThread
  public void detachFromNativeAndReleaseResources() {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    shellHolderLock.writeLock().lock();
    try {
      nativeDestroy(nativeShellHolderId);
      nativeShellHolderId = null;
    } finally {
      shellHolderLock.writeLock().unlock();
    }
  }

  private native void nativeDestroy(long nativeShellHolderId);

  private void ensureNotAttachedToNative() {
    if (nativeShellHolderId != null) {
      throw new RuntimeException(
          "Cannot execute operation because FlutterJNI is attached to native.");
    }
  }

  private void ensureAttachedToNative() {
    if (nativeShellHolderId == null) {
      throw new RuntimeException(
          "Cannot execute operation because FlutterJNI is not attached to native.");
    }
  }
  // ------ End Native Attach/Detach Support ----

  // ----- Start Render Surface Support -----
  /**
   * Adds a {@link FlutterUiDisplayListener}, which receives a callback when Flutter's engine
   * notifies {@code FlutterJNI} that Flutter is painting pixels to the {@link Surface} that was
   * provided to Flutter.
   */
  @UiThread
  public void addIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
    ensureRunningOnMainThread();
    flutterUiDisplayListeners.add(listener);
  }

  /**
   * Removes a {@link FlutterUiDisplayListener} that was added with {@link
   * #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
   */
  @UiThread
  public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
    ensureRunningOnMainThread();
    flutterUiDisplayListeners.remove(listener);
  }

  public static native void nativeImageHeaderCallback(
      long imageGeneratorPointer, int width, int height);

  /**
   * Called by native as a fallback method of image decoding. There are other ways to decode images
   * on lower API levels, they involve copying the native data _and_ do not support any additional
   * formats, whereas ImageDecoder supports HEIF images. Unlike most other methods called from
   * native, this method is expected to be called on a worker thread, since it only uses thread safe
   * methods and may take multiple frames to complete.
   */
  @SuppressWarnings("unused")
  @VisibleForTesting
  @Nullable
  public static Bitmap decodeImage(@NonNull ByteBuffer buffer, long imageGeneratorAddress) {
    if (Build.VERSION.SDK_INT >= 28) {
      ImageDecoder.Source source = ImageDecoder.createSource(buffer);
      try {
        return ImageDecoder.decodeBitmap(
            source,
            (decoder, info, src) -> {
              // i.e. ARGB_8888
              decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB));
              // TODO(bdero): Switch to ALLOCATOR_HARDWARE for devices that have
              // `AndroidBitmap_getHardwareBuffer` (API 30+) available once Skia supports
              // `SkImage::MakeFromAHardwareBuffer` via dynamic lookups:
              // https://skia-review.googlesource.com/c/skia/+/428960
              decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);

              Size size = info.getSize();
              nativeImageHeaderCallback(imageGeneratorAddress, size.getWidth(), size.getHeight());
            });
      } catch (IOException e) {
        Log.e(TAG, "Failed to decode image", e);
        return null;
      }
    }
    return null;
  }

  // Called by native to notify first Flutter frame rendered.
  @SuppressWarnings("unused")
  @VisibleForTesting
  @UiThread
  public void onFirstFrame() {
    ensureRunningOnMainThread();

    for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
      listener.onFlutterUiDisplayed();
    }
  }

  // TODO(mattcarroll): get native to call this when rendering stops.
  @VisibleForTesting
  @UiThread
  void onRenderingStopped() {
    ensureRunningOnMainThread();

    for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
      listener.onFlutterUiNoLongerDisplayed();
    }
  }

  /**
   * Call this method when a {@link Surface} has been created onto which you would like Flutter to
   * paint.
   *
   * <p>See {@link android.view.SurfaceHolder.Callback#surfaceCreated(SurfaceHolder)} for an example
   * of where this call might originate.
   */
  @UiThread
  public void onSurfaceCreated(@NonNull Surface surface) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSurfaceCreated(nativeShellHolderId, surface);
  }

  private native void nativeSurfaceCreated(long nativeShellHolderId, @NonNull Surface surface);

  /**
   * In hybrid composition, call this method when the {@link Surface} has changed.
   *
   * <p>In hybrid composition, the root surfaces changes from {@link
   * android.view.SurfaceHolder#getSurface()} to {@link android.media.ImageReader#getSurface()} when
   * a platform view is in the current frame.
   */
  @UiThread
  public void onSurfaceWindowChanged(@NonNull Surface surface) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSurfaceWindowChanged(nativeShellHolderId, surface);
  }

  private native void nativeSurfaceWindowChanged(
      long nativeShellHolderId, @NonNull Surface surface);

  /**
   * Call this method when the {@link Surface} changes that was previously registered with {@link
   * #onSurfaceCreated(Surface)}.
   *
   * <p>See {@link android.view.SurfaceHolder.Callback#surfaceChanged(SurfaceHolder, int, int, int)}
   * for an example of where this call might originate.
   */
  @UiThread
  public void onSurfaceChanged(int width, int height) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSurfaceChanged(nativeShellHolderId, width, height);
  }

  private native void nativeSurfaceChanged(long nativeShellHolderId, int width, int height);

  /**
   * Call this method when the {@link Surface} is destroyed that was previously registered with
   * {@link #onSurfaceCreated(Surface)}.
   *
   * <p>See {@link android.view.SurfaceHolder.Callback#surfaceDestroyed(SurfaceHolder)} for an
   * example of where this call might originate.
   */
  @UiThread
  public void onSurfaceDestroyed() {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    onRenderingStopped();
    nativeSurfaceDestroyed(nativeShellHolderId);
  }

  private native void nativeSurfaceDestroyed(long nativeShellHolderId);

  /**
   * Call this method to notify Flutter of the current device viewport metrics that are applies to
   * the Flutter UI that is being rendered.
   *
   * <p>This method should be invoked with initial values upon attaching to native. Then, it should
   * be invoked any time those metrics change while {@code FlutterJNI} is attached to native.
   */
  @UiThread
  public void setViewportMetrics(
      float devicePixelRatio,
      int physicalWidth,
      int physicalHeight,
      int physicalPaddingTop,
      int physicalPaddingRight,
      int physicalPaddingBottom,
      int physicalPaddingLeft,
      int physicalViewInsetTop,
      int physicalViewInsetRight,
      int physicalViewInsetBottom,
      int physicalViewInsetLeft,
      int systemGestureInsetTop,
      int systemGestureInsetRight,
      int systemGestureInsetBottom,
      int systemGestureInsetLeft,
      int physicalTouchSlop,
      int[] displayFeaturesBounds,
      int[] displayFeaturesType,
      int[] displayFeaturesState) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSetViewportMetrics(
        nativeShellHolderId,
        devicePixelRatio,
        physicalWidth,
        physicalHeight,
        physicalPaddingTop,
        physicalPaddingRight,
        physicalPaddingBottom,
        physicalPaddingLeft,
        physicalViewInsetTop,
        physicalViewInsetRight,
        physicalViewInsetBottom,
        physicalViewInsetLeft,
        systemGestureInsetTop,
        systemGestureInsetRight,
        systemGestureInsetBottom,
        systemGestureInsetLeft,
        physicalTouchSlop,
        displayFeaturesBounds,
        displayFeaturesType,
        displayFeaturesState);
  }

  private native void nativeSetViewportMetrics(
      long nativeShellHolderId,
      float devicePixelRatio,
      int physicalWidth,
      int physicalHeight,
      int physicalPaddingTop,
      int physicalPaddingRight,
      int physicalPaddingBottom,
      int physicalPaddingLeft,
      int physicalViewInsetTop,
      int physicalViewInsetRight,
      int physicalViewInsetBottom,
      int physicalViewInsetLeft,
      int systemGestureInsetTop,
      int systemGestureInsetRight,
      int systemGestureInsetBottom,
      int systemGestureInsetLeft,
      int physicalTouchSlop,
      int[] displayFeaturesBounds,
      int[] displayFeaturesType,
      int[] displayFeaturesState);
  // ----- End Render Surface Support -----

  // ------ Start Touch Interaction Support ---
  /** Sends a packet of pointer data to Flutter's engine. */
  @UiThread
  public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeDispatchPointerDataPacket(nativeShellHolderId, buffer, position);
  }

  private native void nativeDispatchPointerDataPacket(
      long nativeShellHolderId, @NonNull ByteBuffer buffer, int position);
  // ------ End Touch Interaction Support ---

  @UiThread
  public void setPlatformViewsController(@NonNull PlatformViewsController platformViewsController) {
    ensureRunningOnMainThread();
    this.platformViewsController = platformViewsController;
  }

  // ------ Start Accessibility Support -----
  /**
   * Sets the {@link AccessibilityDelegate} for the attached Flutter context.
   *
   * <p>The {@link AccessibilityDelegate} is responsible for maintaining an Android-side cache of
   * Flutter's semantics tree and custom accessibility actions. This cache should be hooked up to
   * Android's accessibility system.
   *
   * <p>See {@link AccessibilityBridge} for an example of an {@link AccessibilityDelegate} and the
   * surrounding responsibilities.
   */
  @UiThread
  public void setAccessibilityDelegate(@Nullable AccessibilityDelegate accessibilityDelegate) {
    ensureRunningOnMainThread();
    this.accessibilityDelegate = accessibilityDelegate;
  }

  /**
   * Invoked by native to send semantics tree updates from Flutter to Android.
   *
   * <p>The {@code buffer} and {@code strings} form a communication protocol that is implemented
   * here:
   * https://github.com/flutter/engine/blob/main/shell/platform/android/platform_view_android.cc#L207
   */
  @SuppressWarnings("unused")
  @UiThread
  private void updateSemantics(
      @NonNull ByteBuffer buffer,
      @NonNull String[] strings,
      @NonNull ByteBuffer[] stringAttributeArgs) {
    ensureRunningOnMainThread();
    if (accessibilityDelegate != null) {
      accessibilityDelegate.updateSemantics(buffer, strings, stringAttributeArgs);
    }
    // TODO(mattcarroll): log dropped messages when in debug mode
    // (https://github.com/flutter/flutter/issues/25391)
  }

  /**
   * Invoked by native to send new custom accessibility events from Flutter to Android.
   *
   * <p>The {@code buffer} and {@code strings} form a communication protocol that is implemented
   * here:
   * https://github.com/flutter/engine/blob/main/shell/platform/android/platform_view_android.cc#L207
   *
   * <p>// TODO(cbracken): expand these docs to include more actionable information.
   */
  @SuppressWarnings("unused")
  @UiThread
  private void updateCustomAccessibilityActions(
      @NonNull ByteBuffer buffer, @NonNull String[] strings) {
    ensureRunningOnMainThread();
    if (accessibilityDelegate != null) {
      accessibilityDelegate.updateCustomAccessibilityActions(buffer, strings);
    }
    // TODO(mattcarroll): log dropped messages when in debug mode
    // (https://github.com/flutter/flutter/issues/25391)
  }

  /** Sends a semantics action to Flutter's engine, without any additional arguments. */
  public void dispatchSemanticsAction(int nodeId, @NonNull AccessibilityBridge.Action action) {
    dispatchSemanticsAction(nodeId, action, null);
  }

  /** Sends a semantics action to Flutter's engine, with additional arguments. */
  public void dispatchSemanticsAction(
      int nodeId, @NonNull AccessibilityBridge.Action action, @Nullable Object args) {
    ensureAttachedToNative();

    ByteBuffer encodedArgs = null;
    int position = 0;
    if (args != null) {
      encodedArgs = StandardMessageCodec.INSTANCE.encodeMessage(args);
      position = encodedArgs.position();
    }
    dispatchSemanticsAction(nodeId, action.value, encodedArgs, position);
  }

  /**
   * Sends a semantics action to Flutter's engine, given arguments that are already encoded for the
   * engine.
   *
   * <p>To send a semantics action that has not already been encoded, see {@link
   * #dispatchSemanticsAction(int, AccessibilityBridge.Action)} and {@link
   * #dispatchSemanticsAction(int, AccessibilityBridge.Action, Object)}.
   */
  @UiThread
  public void dispatchSemanticsAction(
      int nodeId, int action, @Nullable ByteBuffer args, int argsPosition) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeDispatchSemanticsAction(nativeShellHolderId, nodeId, action, args, argsPosition);
  }

  private native void nativeDispatchSemanticsAction(
      long nativeShellHolderId,
      int nodeId,
      int action,
      @Nullable ByteBuffer args,
      int argsPosition);

  /**
   * Instructs Flutter to enable/disable its semantics tree, which is used by Flutter to support
   * accessibility and related behaviors.
   */
  @UiThread
  public void setSemanticsEnabled(boolean enabled) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSetSemanticsEnabled(nativeShellHolderId, enabled);
  }

  private native void nativeSetSemanticsEnabled(long nativeShellHolderId, boolean enabled);

  // TODO(mattcarroll): figure out what flags are supported and add javadoc about when/why/where to
  // use this.
  @UiThread
  public void setAccessibilityFeatures(int flags) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeSetAccessibilityFeatures(nativeShellHolderId, flags);
  }

  private native void nativeSetAccessibilityFeatures(long nativeShellHolderId, int flags);
  // ------ End Accessibility Support ----

  // ------ Start Texture Registration Support -----
  /**
   * Gives control of a {@link SurfaceTexture} to Flutter so that Flutter can display that texture
   * within Flutter's UI.
   */
  @UiThread
  public void registerTexture(long textureId, @NonNull SurfaceTextureWrapper textureWrapper) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeRegisterTexture(
        nativeShellHolderId, textureId, new WeakReference<SurfaceTextureWrapper>(textureWrapper));
  }

  private native void nativeRegisterTexture(
      long nativeShellHolderId,
      long textureId,
      @NonNull WeakReference<SurfaceTextureWrapper> textureWrapper);

  /**
   * Registers a ImageTexture with the given id.
   *
   * <p>REQUIRED: Callers should eventually unregisterTexture with the same id.
   */
  @UiThread
  public void registerImageTexture(
      long textureId, @NonNull TextureRegistry.ImageTextureEntry imageTextureEntry) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeRegisterImageTexture(
        nativeShellHolderId,
        textureId,
        new WeakReference<TextureRegistry.ImageTextureEntry>(imageTextureEntry));
  }

  private native void nativeRegisterImageTexture(
      long nativeShellHolderId,
      long textureId,
      @NonNull WeakReference<TextureRegistry.ImageTextureEntry> imageTextureEntry);

  /**
   * Call this method to inform Flutter that a texture previously registered with {@link
   * #registerTexture(long, SurfaceTextureWrapper)} has a new frame available.
   *
   * <p>Invoking this method instructs Flutter to update its presentation of the given texture so
   * that the new frame is displayed.
   */
  @UiThread
  public void markTextureFrameAvailable(long textureId) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeMarkTextureFrameAvailable(nativeShellHolderId, textureId);
  }

  private native void nativeMarkTextureFrameAvailable(long nativeShellHolderId, long textureId);

  /**
   * Unregisters a texture that was registered with {@link #registerTexture(long,
   * SurfaceTextureWrapper)}.
   */
  @UiThread
  public void unregisterTexture(long textureId) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeUnregisterTexture(nativeShellHolderId, textureId);
  }

  private native void nativeUnregisterTexture(long nativeShellHolderId, long textureId);
  // ------ Start Texture Registration Support -----

  // ------ Start Dart Execution Support -------
  /**
   * Executes a Dart entrypoint.
   *
   * <p>This can only be done once per JNI attachment because a Dart isolate can only be entered
   * once.
   */
  @UiThread
  public void runBundleAndSnapshotFromLibrary(
      @NonNull String bundlePath,
      @Nullable String entrypointFunctionName,
      @Nullable String pathToEntrypointFunction,
      @NonNull AssetManager assetManager,
      @Nullable List<String> entrypointArgs) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeRunBundleAndSnapshotFromLibrary(
        nativeShellHolderId,
        bundlePath,
        entrypointFunctionName,
        pathToEntrypointFunction,
        assetManager,
        entrypointArgs);
  }

  private native void nativeRunBundleAndSnapshotFromLibrary(
      long nativeShellHolderId,
      @NonNull String bundlePath,
      @Nullable String entrypointFunctionName,
      @Nullable String pathToEntrypointFunction,
      @NonNull AssetManager manager,
      @Nullable List<String> entrypointArgs);
  // ------ End Dart Execution Support -------

  // --------- Start Platform Message Support ------
  /**
   * Sets the handler for all platform messages that come from the attached platform view to Java.
   *
   * <p>Communication between a specific Flutter context (Dart) and the host platform (Java) is
   * accomplished by passing messages. Messages can be sent from Java to Dart with the corresponding
   * {@code FlutterJNI} methods:
   *
   * <ul>
   *   <li>{@link #dispatchPlatformMessage(String, ByteBuffer, int, int)}
   *   <li>{@link #dispatchEmptyPlatformMessage(String, int)}
   * </ul>
   *
   * <p>{@code FlutterJNI} is also the recipient of all platform messages sent from its attached
   * Flutter context. {@code FlutterJNI} does not know what to do with these messages, so a handler
   * is exposed to allow these messages to be processed in whatever manner is desired:
   *
   * <p>{@code setPlatformMessageHandler(PlatformMessageHandler)}
   *
   * <p>If a message is received but no {@link PlatformMessageHandler} is registered, that message
   * will be dropped (ignored). Therefore, when using {@code FlutterJNI} to integrate a Flutter
   * context in an app, a {@link PlatformMessageHandler} must be registered for 2-way Java/Dart
   * communication to operate correctly. Moreover, the handler must be implemented such that
   * fundamental platform messages are handled as expected. See {@link
   * io.flutter.view.FlutterNativeView} for an example implementation.
   */
  @UiThread
  public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformMessageHandler) {
    ensureRunningOnMainThread();
    this.platformMessageHandler = platformMessageHandler;
  }

  private native void nativeCleanupMessageData(long messageData);

  /**
   * Destroys the resources provided sent to `handlePlatformMessage`.
   *
   * <p>This can be called on any thread.
   *
   * @param messageData the argument sent to handlePlatformMessage.
   */
  public void cleanupMessageData(long messageData) {
    // This doesn't rely on being attached like other methods.
    nativeCleanupMessageData(messageData);
  }

  // Called by native on any thread.
  // TODO(mattcarroll): determine if message is nonull or nullable
  @SuppressWarnings("unused")
  @VisibleForTesting
  public void handlePlatformMessage(
      @NonNull final String channel,
      ByteBuffer message,
      final int replyId,
      final long messageData) {
    if (platformMessageHandler != null) {
      platformMessageHandler.handleMessageFromDart(channel, message, replyId, messageData);
    } else {
      nativeCleanupMessageData(messageData);
    }
    // TODO(mattcarroll): log dropped messages when in debug mode
    // (https://github.com/flutter/flutter/issues/25391)
  }

  // Called by native to respond to a platform message that we sent.
  // TODO(mattcarroll): determine if reply is nonull or nullable
  @SuppressWarnings("unused")
  private void handlePlatformMessageResponse(int replyId, ByteBuffer reply) {
    if (platformMessageHandler != null) {
      platformMessageHandler.handlePlatformMessageResponse(replyId, reply);
    }
    // TODO(mattcarroll): log dropped messages when in debug mode
    // (https://github.com/flutter/flutter/issues/25391)
  }

  /**
   * Sends an empty reply (identified by {@code responseId}) from Android to Flutter over the given
   * {@code channel}.
   */
  @UiThread
  public void dispatchEmptyPlatformMessage(@NonNull String channel, int responseId) {
    ensureRunningOnMainThread();
    if (isAttached()) {
      nativeDispatchEmptyPlatformMessage(nativeShellHolderId, channel, responseId);
    } else {
      Log.w(
          TAG,
          "Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: "
              + channel
              + ". Response ID: "
              + responseId);
    }
  }

  // Send an empty platform message to Dart.
  private native void nativeDispatchEmptyPlatformMessage(
      long nativeShellHolderId, @NonNull String channel, int responseId);

  /** Sends a reply {@code message} from Android to Flutter over the given {@code channel}. */
  @UiThread
  public void dispatchPlatformMessage(
      @NonNull String channel, @Nullable ByteBuffer message, int position, int responseId) {
    ensureRunningOnMainThread();
    if (isAttached()) {
      nativeDispatchPlatformMessage(nativeShellHolderId, channel, message, position, responseId);
    } else {
      Log.w(
          TAG,
          "Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: "
              + channel
              + ". Response ID: "
              + responseId);
    }
  }

  // Send a data-carrying platform message to Dart.
  private native void nativeDispatchPlatformMessage(
      long nativeShellHolderId,
      @NonNull String channel,
      @Nullable ByteBuffer message,
      int position,
      int responseId);

  // TODO(mattcarroll): differentiate between channel responses and platform responses.
  public void invokePlatformMessageEmptyResponseCallback(int responseId) {
    // Called on any thread.
    shellHolderLock.readLock().lock();
    try {
      if (isAttached()) {
        nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId);
      } else {
        Log.w(
            TAG,
            "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: "
                + responseId);
      }
    } finally {
      shellHolderLock.readLock().unlock();
    }
  }

  // Send an empty response to a platform message received from Dart.
  private native void nativeInvokePlatformMessageEmptyResponseCallback(
      long nativeShellHolderId, int responseId);

  // TODO(mattcarroll): differentiate between channel responses and platform responses.
  public void invokePlatformMessageResponseCallback(
      int responseId, @NonNull ByteBuffer message, int position) {
    // Called on any thread.
    if (!message.isDirect()) {
      throw new IllegalArgumentException("Expected a direct ByteBuffer.");
    }
    shellHolderLock.readLock().lock();
    try {
      if (isAttached()) {
        nativeInvokePlatformMessageResponseCallback(
            nativeShellHolderId, responseId, message, position);
      } else {
        Log.w(
            TAG,
            "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: "
                + responseId);
      }
    } finally {
      shellHolderLock.readLock().unlock();
    }
  }

  // Send a data-carrying response to a platform message received from Dart.
  private native void nativeInvokePlatformMessageResponseCallback(
      long nativeShellHolderId, int responseId, @Nullable ByteBuffer message, int position);
  // ------- End Platform Message Support ----

  // ----- Start Engine Lifecycle Support ----
  /**
   * Adds the given {@code engineLifecycleListener} to be notified of Flutter engine lifecycle
   * events, e.g., {@link EngineLifecycleListener#onPreEngineRestart()}.
   */
  @UiThread
  public void addEngineLifecycleListener(@NonNull EngineLifecycleListener engineLifecycleListener) {
    ensureRunningOnMainThread();
    engineLifecycleListeners.add(engineLifecycleListener);
  }

  /**
   * Removes the given {@code engineLifecycleListener}, which was previously added using {@link
   * #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
   */
  @UiThread
  public void removeEngineLifecycleListener(
      @NonNull EngineLifecycleListener engineLifecycleListener) {
    ensureRunningOnMainThread();
    engineLifecycleListeners.remove(engineLifecycleListener);
  }

  // Called by native.
  @SuppressWarnings("unused")
  private void onPreEngineRestart() {
    for (EngineLifecycleListener listener : engineLifecycleListeners) {
      listener.onPreEngineRestart();
    }
  }

  @SuppressWarnings("unused")
  @UiThread
  public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) {
    ensureRunningOnMainThread();
    if (platformViewsController == null) {
      throw new RuntimeException(
          "platformViewsController must be set before attempting to position an overlay surface");
    }
    platformViewsController.onDisplayOverlaySurface(id, x, y, width, height);
  }

  @SuppressWarnings("unused")
  @UiThread
  public void onBeginFrame() {
    ensureRunningOnMainThread();
    if (platformViewsController == null) {
      throw new RuntimeException(
          "platformViewsController must be set before attempting to begin the frame");
    }
    platformViewsController.onBeginFrame();
  }

  @SuppressWarnings("unused")
  @UiThread
  public void onEndFrame() {
    ensureRunningOnMainThread();
    if (platformViewsController == null) {
      throw new RuntimeException(
          "platformViewsController must be set before attempting to end the frame");
    }
    platformViewsController.onEndFrame();
  }

  @SuppressWarnings("unused")
  @UiThread
  public FlutterOverlaySurface createOverlaySurface() {
    ensureRunningOnMainThread();
    if (platformViewsController == null) {
      throw new RuntimeException(
          "platformViewsController must be set before attempting to position an overlay surface");
    }
    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 ----

  /** Sets the localization plugin that is used in various localization methods. */
  @UiThread
  public void setLocalizationPlugin(@Nullable LocalizationPlugin localizationPlugin) {
    ensureRunningOnMainThread();
    this.localizationPlugin = localizationPlugin;
  }

  /** Invoked by native to obtain the results of Android's locale resolution algorithm. */
  @SuppressWarnings("unused")
  @VisibleForTesting
  public String[] computePlatformResolvedLocale(@NonNull String[] strings) {
    if (localizationPlugin == null) {
      return new String[0];
    }
    List<Locale> supportedLocales = new ArrayList<Locale>();
    final int localeDataLength = 3;
    for (int i = 0; i < strings.length; i += localeDataLength) {
      String languageCode = strings[i + 0];
      String countryCode = strings[i + 1];
      String scriptCode = strings[i + 2];
      // Convert to Locales via LocaleBuilder if available (API 21+) to include scriptCode.
      if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        Locale.Builder localeBuilder = new Locale.Builder();
        if (!languageCode.isEmpty()) {
          localeBuilder.setLanguage(languageCode);
        }
        if (!countryCode.isEmpty()) {
          localeBuilder.setRegion(countryCode);
        }
        if (!scriptCode.isEmpty()) {
          localeBuilder.setScript(scriptCode);
        }
        supportedLocales.add(localeBuilder.build());
      } else {
        // Pre-API 21, we fall back on scriptCode-less locales.
        supportedLocales.add(new Locale(languageCode, countryCode));
      }
    }

    Locale result = localizationPlugin.resolveNativeLocale(supportedLocales);

    if (result == null) {
      return new String[0];
    }
    String[] output = new String[localeDataLength];
    output[0] = result.getLanguage();
    output[1] = result.getCountry();
    if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
      output[2] = result.getScript();
    } else {
      output[2] = "";
    }
    return output;
  }

  // ----- End Localization Support ----

  // ----- Start Deferred Components Support ----

  /** Sets the deferred component manager that is used to download and install split features. */
  @UiThread
  public void setDeferredComponentManager(
      @Nullable DeferredComponentManager deferredComponentManager) {
    ensureRunningOnMainThread();
    this.deferredComponentManager = deferredComponentManager;
    if (deferredComponentManager != null) {
      deferredComponentManager.setJNI(this);
    }
  }

  /**
   * Called by dart to request that a Dart deferred library corresponding to loadingUnitId be
   * downloaded (if necessary) and loaded into the dart vm.
   *
   * <p>This method delegates the task to DeferredComponentManager, which handles the download and
   * loading of the dart library and any assets.
   *
   * @param loadingUnitId The loadingUnitId is assigned during compile time by gen_snapshot and is
   *     automatically retrieved when loadLibrary() is called on a dart deferred library.
   */
  @SuppressWarnings("unused")
  @UiThread
  public void requestDartDeferredLibrary(int loadingUnitId) {
    if (deferredComponentManager != null) {
      deferredComponentManager.installDeferredComponent(loadingUnitId, null);
    } else {
      // TODO(garyq): Add link to setup/instructions guide wiki.
      Log.e(
          TAG,
          "No DeferredComponentManager found. Android setup must be completed before using split AOT deferred components.");
    }
  }

  /**
   * Searches each of the provided paths for a valid Dart shared library .so file and resolves
   * symbols to load into the dart VM.
   *
   * <p>Successful loading of the dart library completes the future returned by loadLibrary() that
   * triggered the install/load process.
   *
   * @param loadingUnitId The loadingUnitId is assigned during compile time by gen_snapshot and is
   *     automatically retrieved when loadLibrary() is called on a dart deferred library. This is
   *     used to identify which Dart deferred library the resolved correspond to.
   * @param searchPaths An array of paths in which to look for valid dart shared libraries. This
   *     supports paths within zipped apks as long as the apks are not compressed using the
   *     `path/to/apk.apk!path/inside/apk/lib.so` format. Paths will be tried first to last and ends
   *     when a library is successfully found. When the found library is invalid, no additional
   *     paths will be attempted.
   */
  @UiThread
  public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, searchPaths);
  }

  private native void nativeLoadDartDeferredLibrary(
      long nativeShellHolderId, int loadingUnitId, @NonNull String[] searchPaths);

  /**
   * Adds the specified AssetManager as an APKAssetResolver in the Flutter Engine's AssetManager.
   *
   * <p>This may be used to update the engine AssetManager when a new deferred component is
   * installed and a new Android AssetManager is created with access to new assets.
   *
   * @param assetManager An android AssetManager that is able to access the newly downloaded assets.
   * @param assetBundlePath The subdirectory that the flutter assets are stored in. The typical
   *     value is `flutter_assets`.
   */
  @UiThread
  public void updateJavaAssetManager(
      @NonNull AssetManager assetManager, @NonNull String assetBundlePath) {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeUpdateJavaAssetManager(nativeShellHolderId, assetManager, assetBundlePath);
  }

  private native void nativeUpdateJavaAssetManager(
      long nativeShellHolderId,
      @NonNull AssetManager assetManager,
      @NonNull String assetBundlePath);

  /**
   * Indicates that a failure was encountered during the Android portion of downloading a dynamic
   * feature module and loading a dart deferred library, which is typically done by
   * DeferredComponentManager.
   *
   * <p>This will inform dart that the future returned by loadLibrary() should complete with an
   * error.
   *
   * @param loadingUnitId The loadingUnitId that corresponds to the dart deferred library that
   *     failed to install.
   * @param error The error message to display.
   * @param isTransient When isTransient is false, new attempts to install will automatically result
   *     in same error in Dart before the request is passed to Android.
   */
  @SuppressWarnings("unused")
  @UiThread
  public void deferredComponentInstallFailure(
      int loadingUnitId, @NonNull String error, boolean isTransient) {
    ensureRunningOnMainThread();
    nativeDeferredComponentInstallFailure(loadingUnitId, error, isTransient);
  }

  private native void nativeDeferredComponentInstallFailure(
      int loadingUnitId, @NonNull String error, boolean isTransient);

  // ----- End Deferred Components Support ----

  // @SuppressWarnings("unused")
  @UiThread
  public void onDisplayPlatformView(
      int viewId,
      int x,
      int y,
      int width,
      int height,
      int viewWidth,
      int viewHeight,
      FlutterMutatorsStack mutatorsStack) {
    ensureRunningOnMainThread();
    if (platformViewsController == null) {
      throw new RuntimeException(
          "platformViewsController must be set before attempting to position a platform view");
    }
    platformViewsController.onDisplayPlatformView(
        viewId, x, y, width, height, viewWidth, viewHeight, mutatorsStack);
  }

  // TODO(mattcarroll): determine if this is nonull or nullable
  @UiThread
  public Bitmap getBitmap() {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    return nativeGetBitmap(nativeShellHolderId);
  }

  // TODO(mattcarroll): determine if this is nonull or nullable
  private native Bitmap nativeGetBitmap(long nativeShellHolderId);

  /**
   * Notifies the Dart VM of a low memory event, or that the application is in a state such that now
   * is an appropriate time to free resources, such as going to the background.
   *
   * <p>This is distinct from sending a SystemChannel message about low memory, which only notifies
   * the running Flutter application.
   */
  @UiThread
  public void notifyLowMemoryWarning() {
    ensureRunningOnMainThread();
    ensureAttachedToNative();
    nativeNotifyLowMemoryWarning(nativeShellHolderId);
  }

  private native void nativeNotifyLowMemoryWarning(long nativeShellHolderId);

  private void ensureRunningOnMainThread() {
    if (Looper.myLooper() != mainLooper) {
      throw new RuntimeException(
          "Methods marked with @UiThread must be executed on the main thread. Current thread: "
              + Thread.currentThread().getName());
    }
  }

  /**
   * Delegate responsible for creating and updating Android-side caches of Flutter's semantics tree
   * and custom accessibility actions.
   *
   * <p>{@link AccessibilityBridge} is an example of an {@code AccessibilityDelegate}.
   */
  public interface AccessibilityDelegate {
    /**
     * Sends new custom accessibility actions from Flutter to Android.
     *
     * <p>Implementers are expected to maintain an Android-side cache of custom accessibility
     * actions. This method provides new actions to add to that cache.
     */
    void updateCustomAccessibilityActions(@NonNull ByteBuffer buffer, @NonNull String[] strings);

    /**
     * Sends new {@code SemanticsNode} information from Flutter to Android.
     *
     * <p>Implementers are expected to maintain an Android-side cache of Flutter's semantics tree.
     * This method provides updates from Flutter for the Android-side semantics tree cache.
     */
    void updateSemantics(
        @NonNull ByteBuffer buffer,
        @NonNull String[] strings,
        @NonNull ByteBuffer[] stringAttributeArgs);
  }

  public interface AsyncWaitForVsyncDelegate {
    void asyncWaitForVsync(final long cookie);
  }
}
