blob: 982b818a1df81abcd034d4dcc95379a7233fb6ad [file] [log] [blame]
// 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 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();
// 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;
/**
* 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 native void nativePrefetchDefaultFontManager();
private static boolean prefetchDefaultFontManagerCalled = false;
/**
* 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 native void nativeInit(
@NonNull Context context,
@NonNull String[] args,
@Nullable String bundlePath,
@NonNull String appStoragePath,
@NonNull String engineCachesPath,
long initTimeMillis);
private static boolean initCalled = false;
// END methods related to FlutterLoader
@Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate;
// This should also be updated by FlutterView when it is attached to a Display.
// The initial value of 0.0 indicates unknown refresh rate.
private static float refreshRateFPS = 0.0f;
// This is set from native code via JNI.
@Nullable private static String observatoryUri;
private native boolean nativeGetIsSoftwareRenderingEnabled();
@UiThread
/**
* Checks launch settings for whether software rendering is requested.
*
* <p>The value is the same per program.
*/
public boolean getIsSoftwareRenderingEnabled() {
return nativeGetIsSoftwareRenderingEnabled();
}
@Nullable
/**
* Observatory 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.
*/
public static String getObservatoryUri() {
return observatoryUri;
}
public static void setRefreshRateFPS(float refreshRateFPS) {
if (FlutterJNI.setRefreshRateFPSCalled) {
Log.w(TAG, "FlutterJNI.setRefreshRateFPS called more than once");
}
FlutterJNI.refreshRateFPS = refreshRateFPS;
FlutterJNI.setRefreshRateFPSCalled = true;
}
private static boolean setRefreshRateFPSCalled = false;
// TODO(mattcarroll): add javadocs
public static 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.");
}
}
// TODO(mattcarroll): add javadocs
public static native void nativeOnVsync(
long frameDelayNanos, long refreshPeriodNanos, long cookie);
// TODO(mattcarroll): add javadocs
@NonNull
public static native FlutterCallbackInformation nativeLookupCallbackInformation(long handle);
// ----- Start FlutterTextUtils Methods ----
public native boolean nativeFlutterTextUtilsIsEmoji(int codePoint);
public native boolean nativeFlutterTextUtilsIsEmojiModifier(int codePoint);
public native boolean nativeFlutterTextUtilsIsEmojiModifierBase(int codePoint);
public native boolean nativeFlutterTextUtilsIsVariationSelector(int codePoint);
public native boolean nativeFlutterTextUtilsIsRegionalIndicator(int 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.
// 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();
}
// ------ 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)} or {@link #setRefreshRateFPS(float)} shouldn't be called again
* on the spawned FlutterJNI instance.
*/
@UiThread
@NonNull
public FlutterJNI spawn(
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@Nullable String initialRoute) {
ensureRunningOnMainThread();
ensureAttachedToNative();
FlutterJNI spawnedJNI =
nativeSpawn(
nativeShellHolderId, entrypointFunctionName, pathToEntrypointFunction, initialRoute);
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);
/**
* 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)}, 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) {
ensureRunningOnMainThread();
ensureAttachedToNative();
nativeSetViewportMetrics(
nativeShellHolderId,
devicePixelRatio,
physicalWidth,
physicalHeight,
physicalPaddingTop,
physicalPaddingRight,
physicalPaddingBottom,
physicalPaddingLeft,
physicalViewInsetTop,
physicalViewInsetRight,
physicalViewInsetBottom,
physicalViewInsetLeft,
systemGestureInsetTop,
systemGestureInsetRight,
systemGestureInsetBottom,
systemGestureInsetLeft,
physicalTouchSlop);
}
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);
// ----- 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/master/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/master/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 id, @NonNull AccessibilityBridge.Action action) {
dispatchSemanticsAction(id, action, null);
}
/** Sends a semantics action to Flutter's engine, with additional arguments. */
public void dispatchSemanticsAction(
int id, @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(id, 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 id, int action, @Nullable ByteBuffer args, int argsPosition) {
ensureRunningOnMainThread();
ensureAttachedToNative();
nativeDispatchSemanticsAction(nativeShellHolderId, id, action, args, argsPosition);
}
private native void nativeDispatchSemanticsAction(
long nativeShellHolderId, int id, 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);
/**
* 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) {
ensureRunningOnMainThread();
ensureAttachedToNative();
nativeRunBundleAndSnapshotFromLibrary(
nativeShellHolderId,
bundlePath,
entrypointFunctionName,
pathToEntrypointFunction,
assetManager);
}
private native void nativeRunBundleAndSnapshotFromLibrary(
long nativeShellHolderId,
@NonNull String bundlePath,
@Nullable String entrypointFunctionName,
@Nullable String pathToEntrypointFunction,
@NonNull AssetManager manager);
// ------ 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 the ui 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 24+) 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 24, 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);
}
/**
* 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();
}
}
}