From 9558ebf35c1bfc55d95cedb4cb5bb94480b93120 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 9 Jun 2026 09:31:51 -0700 Subject: [PATCH] Convert `FabricUIManager` from Java to Kotlin Summary: This diff converts `FabricUIManager.java` to Kotlin as part of the ongoing React Native Android Java-to-Kotlin migration effort. Key changes: - Converted the class from Java to Kotlin with idiomatic patterns - Removed Hungarian notation from all private fields (e.g. `mBinding` -> `uiManagerBinding`, `mDestroyed` -> `destroyed`) - Static fields (`TAG`, `IS_DEVELOPMENT_ENVIRONMENT`) and static initializer moved to `companion object` with `JvmField` for binary compatibility - `eventDispatcher` and `performanceCounters` implemented as `override val` properties matching Kotlin interface definitions - Package-private methods (`setBinding`, `createDispatchCommandMountItemForInterop`) mapped to `internal` - All `!!` operator usages replaced with `checkNotNull`/`requireNotNull` for safer null handling - Nullable `commandArgs` passed through via unchecked cast to preserve original Java null-pass-through behavior - `OptIn(UnstableReactNativeAPI::class)` added at class level Supporting changes required by the migration: - `DevToolsReactPerfLogger.kt`: `private companion object` changed to `internal companion object` since Kotlin enforces companion visibility that Java did not - `SynchronousMountItem.kt`: Import path updated to use `FabricUIManager.Companion.IS_DEVELOPMENT_ENVIRONMENT` - `PreparedLayout.kt`: Changed from `internal` to `public` because `FabricUIManager` public JNI-facing methods return/accept this type - `ReactAndroid.api`: Regenerated to reflect the updated API surface Changelog: [Internal] Differential Revision: D107865206 --- .../ReactAndroid/api/ReactAndroid.api | 41 +- .../react/fabric/DevToolsReactPerfLogger.kt | 2 +- .../react/fabric/FabricUIManager.java | 1630 ----------------- .../facebook/react/fabric/FabricUIManager.kt | 1471 +++++++++++++++ .../mountitems/SynchronousMountItem.kt | 2 +- 5 files changed, 1494 insertions(+), 1652 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index fe791820641..b53692a162b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2223,39 +2223,39 @@ public final class com/facebook/react/fabric/DevToolsReactPerfLogger$FabricCommi } public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/bridge/UIManager, com/facebook/react/fabric/interop/UIBlockViewResolver, com/facebook/react/uimanager/events/SynchronousEventReceiver { + public static final field Companion Lcom/facebook/react/fabric/FabricUIManager$Companion; public static final field IS_DEVELOPMENT_ENVIRONMENT Z public static final field TAG Ljava/lang/String; - public field mDevToolsReactPerfLogger Lcom/facebook/react/fabric/DevToolsReactPerfLogger; + public field devToolsReactPerfLogger Lcom/facebook/react/fabric/DevToolsReactPerfLogger; public fun (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/react/uimanager/ViewManagerRegistry;Lcom/facebook/react/uimanager/events/BatchEventDispatchedListener;)V public fun addRootView (Landroid/view/View;Lcom/facebook/react/bridge/WritableMap;)I - public fun addUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V + public final fun addUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V public fun addUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V - public fun attachRootView (Lcom/facebook/react/fabric/SurfaceHandlerBinding;Landroid/view/View;)V - public fun clearJSResponder ()V - public fun dispatchCommand (IIILcom/facebook/react/bridge/ReadableArray;)V + public final fun clearJSResponder ()V + public final fun dispatchCommand (IIILcom/facebook/react/bridge/ReadableArray;)V public fun dispatchCommand (IILcom/facebook/react/bridge/ReadableArray;)V - public fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public final fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V public fun dispatchCommand (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V - public fun findNextFocusableElement (III)Ljava/lang/Integer; - public fun getColor (I[Ljava/lang/String;)I + public final fun findNextFocusableElement (III)Ljava/lang/Integer; + public final fun getColor (I[Ljava/lang/String;)I public fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher; public fun getPerformanceCounters ()Ljava/util/Map; - public fun getRelativeAncestorList (II)[I - public fun getThemeData (I[F)Z + public final fun getRelativeAncestorList (II)[I + public final fun getThemeData (I[F)Z public fun initialize ()V public fun invalidate ()V public fun markActiveTouchForTag (II)V - public fun measure (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FFFF)J - public fun onAllAnimationsComplete ()V - public fun onAnimationStarted ()V + public final fun measure (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FFFF)J + public final fun onAllAnimationsComplete ()V + public final fun onAnimationStarted ()V public fun onHostDestroy ()V public fun onHostPause ()V public fun onHostResume ()V - public fun onRequestEventBeat ()V - public fun prependUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V + public final fun onRequestEventBeat ()V + public final fun prependUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V public fun profileNextBatch ()V public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V + public final fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZ)V public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZJ)V public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V @@ -2263,17 +2263,18 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String; public fun resolveView (I)Landroid/view/View; public fun sendAccessibilityEvent (II)V - public fun sendAccessibilityEventFromJS (IILjava/lang/String;)V - public fun setJSResponder (IIIZ)V + public final fun sendAccessibilityEventFromJS (IILjava/lang/String;)V + public final fun setJSResponder (IIIZ)V public fun startSurface (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;II)I - public fun startSurface (Lcom/facebook/react/fabric/SurfaceHandlerBinding;Landroid/content/Context;Landroid/view/View;)V public fun stopSurface (I)V - public fun stopSurface (Lcom/facebook/react/fabric/SurfaceHandlerBinding;)V public fun sweepActiveTouchForTag (II)V public fun synchronouslyUpdateViewOnUIThread (ILcom/facebook/react/bridge/ReadableMap;)V public fun updateRootLayoutSpecs (IIIII)V } +public final class com/facebook/react/fabric/FabricUIManager$Companion { +} + public final class com/facebook/react/fabric/FabricUIManagerProviderImpl : com/facebook/react/bridge/UIManagerProvider { public fun (Lcom/facebook/react/fabric/ComponentFactory;Lcom/facebook/react/uimanager/ViewManagerRegistry;)V public fun createUIManager (Lcom/facebook/react/bridge/ReactApplicationContext;)Lcom/facebook/react/bridge/UIManager; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/DevToolsReactPerfLogger.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/DevToolsReactPerfLogger.kt index ff635f2b934..14da182b95c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/DevToolsReactPerfLogger.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/DevToolsReactPerfLogger.kt @@ -150,7 +150,7 @@ public class DevToolsReactPerfLogger : FabricMarkerListener { } } - private companion object { + internal companion object { @JvmField internal val streamingCommitStats: LongStreamingStats = LongStreamingStats() @JvmField internal val streamingLayoutStats: LongStreamingStats = LongStreamingStats() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java deleted file mode 100644 index ee64131ab18..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ /dev/null @@ -1,1630 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.fabric; - -import static com.facebook.infer.annotation.ThreadConfined.ANY; -import static com.facebook.infer.annotation.ThreadConfined.UI; -import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getMaxSize; -import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getMinSize; -import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getYogaMeasureMode; -import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getYogaSize; -import static com.facebook.react.uimanager.UIManagerHelper.PADDING_BOTTOM_INDEX; -import static com.facebook.react.uimanager.UIManagerHelper.PADDING_END_INDEX; -import static com.facebook.react.uimanager.UIManagerHelper.PADDING_START_INDEX; -import static com.facebook.react.uimanager.UIManagerHelper.PADDING_TOP_INDEX; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Point; -import android.os.SystemClock; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import androidx.annotation.AnyThread; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.core.view.ViewCompat.FocusDirection; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.infer.annotation.ThreadConfined; -import com.facebook.proguard.annotations.DoNotStripAny; -import com.facebook.react.bridge.ColorPropConverter; -import com.facebook.react.bridge.GuardedRunnable; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.NativeArray; -import com.facebook.react.bridge.NativeMap; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactMarker; -import com.facebook.react.bridge.ReactMarkerConstants; -import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.UIManager; -import com.facebook.react.bridge.UIManagerListener; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.common.annotations.UnstableReactNativeAPI; -import com.facebook.react.common.build.ReactBuildConfig; -import com.facebook.react.common.mapbuffer.ReadableMapBuffer; -import com.facebook.react.fabric.events.EventEmitterWrapper; -import com.facebook.react.fabric.events.FabricEventEmitter; -import com.facebook.react.fabric.internal.interop.InteropUIBlockListener; -import com.facebook.react.fabric.interop.UIBlock; -import com.facebook.react.fabric.interop.UIBlockViewResolver; -import com.facebook.react.fabric.mounting.MountItemDispatcher; -import com.facebook.react.fabric.mounting.MountingManager; -import com.facebook.react.fabric.mounting.SurfaceMountingManager; -import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; -import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; -import com.facebook.react.fabric.mounting.mountitems.MountItem; -import com.facebook.react.fabric.mounting.mountitems.MountItemFactory; -import com.facebook.react.fabric.mounting.mountitems.PrefetchResourcesMountItem; -import com.facebook.react.fabric.mounting.mountitems.SynchronousMountItem; -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; -import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags; -import com.facebook.react.internal.interop.InteropEventEmitter; -import com.facebook.react.modules.core.ReactChoreographer; -import com.facebook.react.modules.i18nmanager.I18nUtil; -import com.facebook.react.uimanager.DisplayMetricsHolder; -import com.facebook.react.uimanager.GuardedFrameCallback; -import com.facebook.react.uimanager.IllegalViewOperationException; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.ReactRoot; -import com.facebook.react.uimanager.ReactRootViewTagGenerator; -import com.facebook.react.uimanager.RootViewUtil; -import com.facebook.react.uimanager.StateWrapper; -import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIManagerHelper; -import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.uimanager.ViewManagerPropertyUpdater; -import com.facebook.react.uimanager.ViewManagerRegistry; -import com.facebook.react.uimanager.events.BatchEventDispatchedListener; -import com.facebook.react.uimanager.events.EventCategoryDef; -import com.facebook.react.uimanager.events.EventDispatcher; -import com.facebook.react.uimanager.events.FabricEventDispatcher; -import com.facebook.react.uimanager.events.RCTEventEmitter; -import com.facebook.react.uimanager.events.SynchronousEventReceiver; -import com.facebook.react.views.text.PreparedLayout; -import com.facebook.react.views.text.ReactTextViewManager; -import com.facebook.react.views.text.ReactTextViewManagerCallback; -import com.facebook.react.views.text.TextEffectRegistry; -import com.facebook.react.views.text.TextLayoutManager; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * We instruct ProGuard not to strip out any fields or methods, because many of these methods are - * only called through the JNI from Cxx so it appears that most of this class is "unused". - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -@SuppressLint("MissingNativeLoadLibrary") -@DoNotStripAny -public class FabricUIManager - implements UIManager, LifecycleEventListener, UIBlockViewResolver, SynchronousEventReceiver { - public static final String TAG = FabricUIManager.class.getSimpleName(); - - // The IS_DEVELOPMENT_ENVIRONMENT variable is used to log extra data when running fabric in a - // development environment. DO NOT ENABLE THIS ON PRODUCTION OR YOU WILL BE FIRED! - @SuppressLint("ClownyBooleanExpression") - public static final boolean IS_DEVELOPMENT_ENVIRONMENT = false && ReactBuildConfig.DEBUG; - - public @Nullable DevToolsReactPerfLogger mDevToolsReactPerfLogger; - - private static final DevToolsReactPerfLogger.DevToolsReactPerfLoggerListener FABRIC_PERF_LOGGER = - commitPoint -> { - long commitDuration = commitPoint.getCommitDuration(); - long layoutDuration = commitPoint.getLayoutDuration(); - long diffDuration = commitPoint.getDiffDuration(); - long transactionEndDuration = commitPoint.getTransactionEndDuration(); - long batchExecutionDuration = commitPoint.getBatchExecutionDuration(); - - DevToolsReactPerfLogger.streamingCommitStats.add(commitDuration); - DevToolsReactPerfLogger.streamingLayoutStats.add(layoutDuration); - DevToolsReactPerfLogger.streamingDiffStats.add(diffDuration); - DevToolsReactPerfLogger.streamingTransactionEndStats.add(transactionEndDuration); - DevToolsReactPerfLogger.streamingBatchExecutionStats.add(batchExecutionDuration); - - FLog.i( - TAG, - "Statistics of Fabric commit #%d:\n" - + " - Total commit time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" - + " - Layout time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" - + " - Diffing time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" - + " - FinishTransaction (Diffing + JNI serialization): %d ms. Avg: %.2f. Median:" - + " %.2f ms. Max: %d ms.\n" - + " - Mounting: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n", - commitPoint.getCommitNumber(), - commitDuration, - DevToolsReactPerfLogger.streamingCommitStats.getAverage(), - DevToolsReactPerfLogger.streamingCommitStats.getMedian(), - DevToolsReactPerfLogger.streamingCommitStats.getMax(), - layoutDuration, - DevToolsReactPerfLogger.streamingLayoutStats.getAverage(), - DevToolsReactPerfLogger.streamingLayoutStats.getMedian(), - DevToolsReactPerfLogger.streamingLayoutStats.getMax(), - diffDuration, - DevToolsReactPerfLogger.streamingDiffStats.getAverage(), - DevToolsReactPerfLogger.streamingDiffStats.getMedian(), - DevToolsReactPerfLogger.streamingDiffStats.getMax(), - transactionEndDuration, - DevToolsReactPerfLogger.streamingTransactionEndStats.getAverage(), - DevToolsReactPerfLogger.streamingTransactionEndStats.getMedian(), - DevToolsReactPerfLogger.streamingTransactionEndStats.getMax(), - batchExecutionDuration, - DevToolsReactPerfLogger.streamingBatchExecutionStats.getAverage(), - DevToolsReactPerfLogger.streamingBatchExecutionStats.getMedian(), - DevToolsReactPerfLogger.streamingBatchExecutionStats.getMax()); - }; - - static { - FabricSoLoader.staticInit(); - } - - @Nullable private FabricUIManagerBinding mBinding; - private final ReactApplicationContext mReactApplicationContext; - private final MountingManager mMountingManager; - private final FabricEventDispatcher mEventDispatcher; - private final MountItemDispatcher mMountItemDispatcher; - private final ViewManagerRegistry mViewManagerRegistry; - - private final TextEffectRegistry mTextEffectRegistry = new TextEffectRegistry(); - - private final BatchEventDispatchedListener mBatchEventDispatchedListener; - - private final List mListeners = new CopyOnWriteArrayList<>(); - - private boolean mMountNotificationScheduled = false; - private List mSurfaceIdsWithPendingMountNotification = new ArrayList<>(); - - @ThreadConfined(UI) - private final DispatchUIFrameCallback mDispatchUIFrameCallback; - - /** Set of events sent synchronously during the current frame render. Cleared after each frame. */ - @ThreadConfined(UI) - private final Set mSynchronousEvents = new HashSet<>(); - - /** - * Queue of surface IDs that need their React revision merged. Drained during doFrame so that - * synchronous events dispatched by the merge are processed in the same frame. - */ - private final ConcurrentLinkedQueue mPendingReactRevisionMerges = - new ConcurrentLinkedQueue<>(); - - /** - * This is used to keep track of whether or not the FabricUIManager has been destroyed. Once the - * Catalyst instance is being destroyed, we should cease all operation here. - */ - private volatile boolean mDestroyed = false; - - private boolean mDriveCxxAnimations = false; - - private @Nullable ViewTransitionSnapshotManager mViewTransitionSnapshotManager; - - private long mDispatchViewUpdatesTime = 0l; - private long mCommitStartTime = 0l; - private long mLayoutTime = 0l; - private long mFinishTransactionTime = 0l; - private long mFinishTransactionCPPTime = 0l; - - // C++ keeps track of commit numbers for telemetry purposes. We don't want to incur a JNI - // round-trip cost just for this, so commits from C++ are numbered 0+ and synchronous commits - // are 10k+. Since these are only used for perf tracking, it's unlikely for the number of commits - // from C++ to exceed 9,999 and it should be obvious what's going on when analyzing performance. - private int mCurrentSynchronousCommitNumber = 10000; - - private final MountingManager.MountItemExecutor mMountItemExecutor = - new MountingManager.MountItemExecutor() { - @Override - public void executeItems(Queue items) { - // This executor can be technically accessed before the dispatcher is created, - // but if that happens, something is terribly wrong - mMountItemDispatcher.dispatchMountItems(items); - } - }; - - // Interop UIManagerListener used to support addUIBlock and prependUIBlock. - // It's initialized only when addUIBlock or prependUIBlock is called the first time. - @Nullable private InteropUIBlockListener mInteropUIBlockListener; - - public FabricUIManager( - ReactApplicationContext reactContext, - ViewManagerRegistry viewManagerRegistry, - BatchEventDispatchedListener batchEventDispatchedListener) { - mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext); - mReactApplicationContext = reactContext; - mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor); - mMountItemDispatcher = - new MountItemDispatcher(mMountingManager, new MountItemDispatchListener()); - mEventDispatcher = new FabricEventDispatcher(reactContext, new FabricEventEmitter(this)); - mBatchEventDispatchedListener = batchEventDispatchedListener; - mReactApplicationContext.addLifecycleEventListener(this); - - mViewManagerRegistry = viewManagerRegistry; - mReactApplicationContext.registerComponentCallbacks(viewManagerRegistry); - } - - @Override - @UiThread - @ThreadConfined(UI) - @Deprecated - public int addRootView( - final T rootView, final @Nullable WritableMap initialProps) { - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalViewOperationException( - "Do not call addRootView in Fabric; it is unsupported. Call startSurface instead.")); - - ReactRoot reactRootView = (ReactRoot) rootView; - final int rootTag = reactRootView.getRootViewTag(); - - ThemedReactContext reactContext = - new ThemedReactContext( - mReactApplicationContext, rootView.getContext(), reactRootView.getSurfaceID(), rootTag); - mMountingManager.startSurface(rootTag, reactContext, rootView); - String moduleName = reactRootView.getJSModuleName(); - if (ReactNativeFeatureFlags.enableFabricLogs()) { - FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag); - } - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.startSurface(rootTag, moduleName, (NativeMap) initialProps); - return rootTag; - } - - /** - * Find the next focusable element's id and position relative to the parent from the shadow tree - * based on the current focusable element and the direction. - * - * @return A NextFocusableNode object where the 'id' is the reactId/Tag of the next focusable - * view, returns null if no view could be found - */ - public @Nullable Integer findNextFocusableElement( - int parentTag, int focusedTag, @FocusDirection int direction) { - if (mBinding == null) { - return null; - } - - int generalizedDirection; - - switch (direction) { - case View.FOCUS_DOWN: - generalizedDirection = 0; - break; - case View.FOCUS_UP: - generalizedDirection = 1; - break; - case View.FOCUS_RIGHT: - generalizedDirection = 2; - break; - case View.FOCUS_LEFT: - generalizedDirection = 3; - break; - case View.FOCUS_FORWARD: - generalizedDirection = 4; - break; - case View.FOCUS_BACKWARD: - generalizedDirection = 5; - break; - default: - return null; - } - - int serializedNextFocusableNodeMetrics = - mBinding.findNextFocusableElement(parentTag, focusedTag, generalizedDirection); - - if (serializedNextFocusableNodeMetrics == -1) { - return null; - } - - return serializedNextFocusableNodeMetrics; - } - - public @Nullable int[] getRelativeAncestorList(int rootTag, int childTag) { - return mBinding != null ? mBinding.getRelativeAncestorList(rootTag, childTag) : null; - } - - @Override - @AnyThread - @ThreadConfined(ANY) - public int startSurface( - final T rootView, - final String moduleName, - final @Nullable WritableMap initialProps, - int widthMeasureSpec, - int heightMeasureSpec) { - final int rootTag = ((ReactRoot) rootView).getRootViewTag(); - Context context = rootView.getContext(); - ThemedReactContext reactContext = - new ThemedReactContext(mReactApplicationContext, context, moduleName, rootTag); - if (ReactNativeFeatureFlags.enableFabricLogs()) { - FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag); - } - mMountingManager.startSurface(rootTag, reactContext, rootView); - - // If startSurface is executed in the UIThread then, it uses the ViewportOffset from the View, - // Otherwise Fabric relies on calling {@link Binding#setConstraints} method to update the - // ViewportOffset during measurement or onLayout. - @SuppressLint("WrongThread") - Point viewportOffset = - UiThreadUtil.isOnUiThread() ? RootViewUtil.getViewportOffset(rootView) : new Point(0, 0); - - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.startSurfaceWithConstraints( - rootTag, - moduleName, - (NativeMap) initialProps, - getMinSize(widthMeasureSpec), - getMaxSize(widthMeasureSpec), - getMinSize(heightMeasureSpec), - getMaxSize(heightMeasureSpec), - viewportOffset.x, - viewportOffset.y, - I18nUtil.getInstance().isRTL(context), - I18nUtil.getInstance().doLeftAndRightSwapInRTL(context)); - return rootTag; - } - - public void startSurface( - final SurfaceHandlerBinding surfaceHandler, - final Context context, - final @Nullable View rootView) { - final int rootTag = - rootView instanceof ReactRoot - ? ((ReactRoot) rootView).getRootViewTag() - : ReactRootViewTagGenerator.getNextRootViewTag(); - - ThemedReactContext reactContext = - new ThemedReactContext( - mReactApplicationContext, context, surfaceHandler.getModuleName(), rootTag); - mMountingManager.startSurface(rootTag, reactContext, rootView); - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.startSurfaceWithSurfaceHandler(rootTag, surfaceHandler, rootView != null); - } - - public void attachRootView(final SurfaceHandlerBinding surfaceHandler, final View rootView) { - ThemedReactContext reactContext = - new ThemedReactContext( - mReactApplicationContext, - rootView.getContext(), - surfaceHandler.getModuleName(), - surfaceHandler.getSurfaceId()); - mMountingManager.attachRootView(surfaceHandler.getSurfaceId(), rootView, reactContext); - - surfaceHandler.setMountable(true); - } - - public void stopSurface(final SurfaceHandlerBinding surfaceHandler) { - if (!surfaceHandler.isRunning()) { - ReactSoftExceptionLogger.logSoftException( - FabricUIManager.TAG, - new IllegalStateException("Trying to stop surface that hasn't started yet")); - return; - } - - mMountingManager.stopSurface(surfaceHandler.getSurfaceId()); - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.stopSurfaceWithSurfaceHandler(surfaceHandler); - } - - /** Method called when an event has been dispatched on the C++ side. */ - @SuppressWarnings("unused") - public void onRequestEventBeat() { - mEventDispatcher.dispatchAllEvents(); - } - - @AnyThread - @ThreadConfined(ANY) - @Override - public void stopSurface(final int surfaceID) { - // Mark surfaceId as dead, stop executing mounting instructions - mMountingManager.stopSurface(surfaceID); - - // Communicate stopSurface to Cxx - causes an empty ShadowTree to be committed, - // but all mounting instructions will be ignored because stopSurface was called - // on the MountingManager - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.stopSurface(surfaceID); - } - - @Override - public void initialize() { - mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener); - if (ReactNativeFeatureFlags.enableFabricLogs()) { - mDevToolsReactPerfLogger = new DevToolsReactPerfLogger(); - mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER); - - ReactMarker.addFabricListener(mDevToolsReactPerfLogger); - } - if (!ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP - && ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext); - mReactApplicationContext.internal_registerInteropModule( - RCTEventEmitter.class, interopEventEmitter); - } - } - - @Override - @AnyThread - @ThreadConfined(ANY) - public void invalidate() { - FLog.i(TAG, "FabricUIManager.invalidate"); - - if (mDevToolsReactPerfLogger != null) { - mDevToolsReactPerfLogger.removeDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER); - ReactMarker.removeFabricListener(mDevToolsReactPerfLogger); - } - - if (mDestroyed) { - ReactSoftExceptionLogger.logSoftException( - FabricUIManager.TAG, new IllegalStateException("Cannot double-destroy FabricUIManager")); - return; - } - - mDestroyed = true; - - mEventDispatcher.removeBatchEventDispatchedListener(mBatchEventDispatchedListener); - mEventDispatcher.invalidate(); - - mReactApplicationContext.unregisterComponentCallbacks(mViewManagerRegistry); - mViewManagerRegistry.invalidate(); - - // Remove lifecycle listeners (onHostResume, onHostPause) since the FabricUIManager is going - // away. Then stop the mDispatchUIFrameCallback false will cause the choreographer - // callbacks to stop firing. - mReactApplicationContext.removeLifecycleEventListener(this); - onHostPause(); - - if (mBinding != null) { - mBinding.unregister(); - } - mBinding = null; - - ViewManagerPropertyUpdater.clear(); - } - - @Override - public void markActiveTouchForTag(int surfaceId, int reactTag) { - SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId); - if (surfaceMountingManager != null) { - surfaceMountingManager.markActiveTouchForTag(reactTag); - } - } - - @Override - public void sweepActiveTouchForTag(int surfaceId, int reactTag) { - SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId); - if (surfaceMountingManager != null) { - surfaceMountingManager.sweepActiveTouchForTag(reactTag); - } - } - - /** - * Method added to Fabric for backward compatibility reasons, as users on Paper could call - * [addUiBlock] and [prependUiBlock] on UIManagerModule. - */ - public void addUIBlock(UIBlock block) { - if (!ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP - && ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - InteropUIBlockListener listener = getInteropUIBlockListener(); - listener.addUIBlock(block); - } - } - - /** - * Method added to Fabric for backward compatibility reasons, as users on Paper could call - * [addUiBlock] and [prependUiBlock] on UIManagerModule. - */ - public void prependUIBlock(UIBlock block) { - if (!ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP - && ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - InteropUIBlockListener listener = getInteropUIBlockListener(); - listener.prependUIBlock(block); - } - } - - private InteropUIBlockListener getInteropUIBlockListener() { - if (mInteropUIBlockListener == null) { - mInteropUIBlockListener = new InteropUIBlockListener(); - addUIManagerEventListener(mInteropUIBlockListener); - } - return mInteropUIBlockListener; - } - - @SuppressWarnings("unused") - private NativeArray measureLines( - ReadableMapBuffer attributedString, - ReadableMapBuffer paragraphAttributes, - float width, - float height) { - ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS); - - return (NativeArray) - TextLayoutManager.measureLines( - mReactApplicationContext.getAssets(), - attributedString, - paragraphAttributes, - PixelUtil.toPixelFromDIP(width), - PixelUtil.toPixelFromDIP(height), - textViewManager instanceof ReactTextViewManagerCallback - ? (ReactTextViewManagerCallback) textViewManager - : null, - mTextEffectRegistry); - } - - public int getColor(int surfaceId, String[] resourcePaths) { - ThemedReactContext context = - mMountingManager.getSurfaceManagerEnforced(surfaceId, "getColor").getContext(); - // Surface may have been stopped - if (context == null) { - return 0; - } - - for (String resourcePath : resourcePaths) { - Integer color = ColorPropConverter.resolveResourcePath(context, resourcePath); - if (color != null) { - return color; - } - } - return 0; - } - - /** - * Calls the measure() function on a specific view manager. This may be used for implementing - * custom Fabric ShadowNodes - */ - @AnyThread - @ThreadConfined(ANY) - public long measure( - int surfaceId, - String componentName, - ReadableMap localData, - ReadableMap props, - ReadableMap state, - float minWidth, - float maxWidth, - float minHeight, - float maxHeight) { - ReactContext context; - if (surfaceId > 0) { - SurfaceMountingManager surfaceMountingManager = - mMountingManager.getSurfaceManagerEnforced(surfaceId, "measure"); - if (surfaceMountingManager.isStopped()) { - return 0; - } - context = surfaceMountingManager.getContext(); - Assertions.assertNotNull( - context, "Context in SurfaceMountingManager is null. surfaceId: " + surfaceId); - } else { - context = mReactApplicationContext; - } - - return mMountingManager.measure( - context, - componentName, - localData, - props, - state, - getYogaSize(minWidth, maxWidth), - getYogaMeasureMode(minWidth, maxWidth), - getYogaSize(minHeight, maxHeight), - getYogaMeasureMode(minHeight, maxHeight), - null); - } - - @AnyThread - @ThreadConfined(ANY) - @UnstableReactNativeAPI - public long measureText( - ReadableMapBuffer attributedString, - ReadableMapBuffer paragraphAttributes, - float minWidth, - float maxWidth, - float minHeight, - float maxHeight, - @Nullable float[] attachmentsPositions) { - - ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS); - - return TextLayoutManager.measureText( - mReactApplicationContext.getAssets(), - attributedString, - paragraphAttributes, - getYogaSize(minWidth, maxWidth), - getYogaMeasureMode(minWidth, maxWidth), - getYogaSize(minHeight, maxHeight), - getYogaMeasureMode(minHeight, maxHeight), - textViewManager instanceof ReactTextViewManagerCallback - ? (ReactTextViewManagerCallback) textViewManager - : null, - attachmentsPositions, - mTextEffectRegistry); - } - - @AnyThread - @ThreadConfined(ANY) - @UnstableReactNativeAPI - public PreparedLayout prepareTextLayout( - ReadableMapBuffer attributedString, - ReadableMapBuffer paragraphAttributes, - float minWidth, - float maxWidth, - float minHeight, - float maxHeight) { - ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS); - - return TextLayoutManager.createPreparedLayout( - mReactApplicationContext.getAssets(), - attributedString, - paragraphAttributes, - getYogaSize(minWidth, maxWidth), - getYogaMeasureMode(minWidth, maxWidth), - getYogaSize(minHeight, maxHeight), - getYogaMeasureMode(minHeight, maxHeight), - textViewManager instanceof ReactTextViewManagerCallback - ? (ReactTextViewManagerCallback) textViewManager - : null, - mTextEffectRegistry); - } - - @AnyThread - @ThreadConfined(ANY) - @UnstableReactNativeAPI - public PreparedLayout reusePreparedLayoutWithNewReactTags( - PreparedLayout preparedLayout, int[] reactTags) { - return new PreparedLayout( - preparedLayout.getLayout(), - preparedLayout.getMaximumNumberOfLines(), - preparedLayout.getVerticalOffset(), - reactTags, - preparedLayout.getTextBreakStrategy(), - preparedLayout.getJustificationMode()); - } - - @AnyThread - @ThreadConfined(ANY) - @UnstableReactNativeAPI - public float[] measurePreparedLayout( - PreparedLayout preparedLayout, - float minWidth, - float maxWidth, - float minHeight, - float maxHeight) { - return TextLayoutManager.measurePreparedLayout( - preparedLayout, - getYogaSize(minWidth, maxWidth), - getYogaMeasureMode(minWidth, maxWidth), - getYogaSize(minHeight, maxHeight), - getYogaMeasureMode(minHeight, maxHeight)); - } - - @UnstableReactNativeAPI - public TextEffectRegistry getTextEffectRegistry() { - return mTextEffectRegistry; - } - - /** - * @param surfaceId {@link int} surface ID - * @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme - * padding used by RN Android TextInput. - * @return if theme data is available in the output parameters. - */ - @SuppressWarnings("unused") - public boolean getThemeData(int surfaceId, float[] defaultTextInputPadding) { - SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId); - Context context = surfaceMountingManager != null ? surfaceMountingManager.getContext() : null; - if (context == null) { - FLog.w(TAG, "Couldn't get context for surfaceId %d in getThemeData", surfaceId); - return false; - } - - float[] defaultTextInputPaddingForTheme = UIManagerHelper.getDefaultTextInputPadding(context); - defaultTextInputPadding[0] = defaultTextInputPaddingForTheme[PADDING_START_INDEX]; - defaultTextInputPadding[1] = defaultTextInputPaddingForTheme[PADDING_END_INDEX]; - defaultTextInputPadding[2] = defaultTextInputPaddingForTheme[PADDING_TOP_INDEX]; - defaultTextInputPadding[3] = defaultTextInputPaddingForTheme[PADDING_BOTTOM_INDEX]; - return true; - } - - /** - * This method is used to get the encoded screen size without vertical insets for a given surface. - * It's used by the Modal component to determine the size of the screen without vertical insets. - * The method is private as it's accessed via JNI from C++. - * - * @param surfaceId The surface ID of the surface for which the Modal is going to render. - * @return The encoded screen size as a long (both width and height) are represented without - * vertical insets. - */ - private long getEncodedScreenSizeWithoutVerticalInsets(int surfaceId) { - ThemedReactContext context = - mMountingManager - .getSurfaceManagerEnforced(surfaceId, "getEncodedScreenSizeWithoutVerticalInsets") - .getContext(); - if (context == null) { - FLog.w(TAG, "Couldn't get context from SurfaceMountingManager for surfaceId %d", surfaceId); - return 0; - } else { - return DisplayMetricsHolder.getEncodedScreenSizeWithoutVerticalInsets( - context.getCurrentActivity()); - } - } - - @Override - public void addUIManagerEventListener(UIManagerListener listener) { - mListeners.add(listener); - } - - @Override - public void removeUIManagerEventListener(UIManagerListener listener) { - mListeners.remove(listener); - } - - @Override - @UiThread - @ThreadConfined(UI) - public void synchronouslyUpdateViewOnUIThread(final int reactTag, final ReadableMap props) { - UiThreadUtil.assertOnUiThread(); - - int commitNumber = mCurrentSynchronousCommitNumber++; - - // We are on the UI thread so it would otherwise be safe to call `tryDispatchMountItems` here to - // flush previously-queued mountitems, *BUT* we don't know where we are on the callstack. - // Why isn't it safe, and why do we have additional safeguards here? - // - // A tangible example where it would cause a crash, and did in the past: - // 1. There are queued "delete" mutations - // 2. We're called by this stack trace: - // FabricUIManager.synchronouslyUpdateViewOnUIThread(FabricUIManager.java:574) - // PropsAnimatedNode.updateView(PropsAnimatedNode.java:114) - // NativeAnimatedNodesManager.updateNodes(NativeAnimatedNodesManager.java:655) - // NativeAnimatedNodesManager.handleEvent(NativeAnimatedNodesManager.java:521) - // NativeAnimatedNodesManager.onEventDispatch(NativeAnimatedNodesManager.java:483) - // EventDispatcherImpl.dispatchEvent(EventDispatcherImpl.java:116) - // ReactScrollViewHelper.emitScrollEvent(ReactScrollViewHelper.java:85) - // ReactScrollViewHelper.emitScrollEvent(ReactScrollViewHelper.java:46) - // ReactScrollView.onScrollChanged(ReactScrollView.java:285) - // ReactScrollView.onOverScrolled(ReactScrollView.java:808) - // android.view.View.overScrollBy(View.java:26052) - // android.widget.ScrollView.overScrollBy(ScrollView.java:2040) - // android.widget.ScrollView.computeScroll(ScrollView.java:1481) - // android.view.View.updateDisplayListIfDirty(View.java:20466) - // 3. A view is deleted while its parent is being drawn, causing a crash. - - MountItem synchronousMountItem = new SynchronousMountItem(reactTag, props); - - // If the reactTag exists, we assume that it might at the end of the next - // batch of MountItems. Otherwise, we try to execute immediately. - if (!mMountingManager.getViewExists(reactTag)) { - mMountItemDispatcher.addMountItem(synchronousMountItem); - return; - } - - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, null, commitNumber); - - if (ReactNativeFeatureFlags.enableFabricLogs()) { - FLog.d( - TAG, - "SynchronouslyUpdateViewOnUIThread for tag %d: %s", - reactTag, - (IS_DEVELOPMENT_ENVIRONMENT ? props.toHashMap().toString() : "")); - } - - synchronousMountItem.execute(mMountingManager); - - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); - } - - /** Called from C++ via JNI. */ - @SuppressLint("NotInvokedPrivateMethod") - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void captureViewSnapshot(final int reactTag, final int surfaceId) { - getViewTransitionSnapshotManager().captureViewSnapshot(reactTag, surfaceId); - } - - /** Called from C++ via JNI. */ - @SuppressLint("NotInvokedPrivateMethod") - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void setViewSnapshot(final int sourceTag, final int targetTag, final int surfaceId) { - getViewTransitionSnapshotManager().setViewSnapshot(sourceTag, targetTag); - } - - /** Called from C++ via JNI. */ - @SuppressLint("NotInvokedPrivateMethod") - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void clearPendingSnapshots() { - getViewTransitionSnapshotManager().clearPendingSnapshots(); - } - - private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotManager() { - if (mViewTransitionSnapshotManager == null) { - mViewTransitionSnapshotManager = new ViewTransitionSnapshotManager(this, mMountingManager); - } - return mViewTransitionSnapshotManager; - } - - @SuppressLint("NotInvokedPrivateMethod") - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void preallocateView( - int rootTag, - int reactTag, - String componentName, - Object props, - @Nullable Object stateWrapper, - boolean isLayoutable) { - mMountItemDispatcher.addPreAllocateMountItem( - MountItemFactory.createPreAllocateViewMountItem( - rootTag, - reactTag, - componentName, - (ReadableMap) props, - (StateWrapper) stateWrapper, - isLayoutable)); - } - - @SuppressLint("NotInvokedPrivateMethod") // Called from C++ via JNI - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void destroyUnmountedView(int surfaceId, int reactTag) { - mMountItemDispatcher.addMountItem( - MountItemFactory.createDestroyViewMountItem(surfaceId, reactTag)); - } - - @SuppressLint("NotInvokedPrivateMethod") - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private boolean isOnMainThread() { - return UiThreadUtil.isOnUiThread(); - } - - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private MountItem createIntBufferBatchMountItem( - int rootTag, @Nullable int[] intBuffer, @Nullable Object[] objBuffer, int commitNumber) { - return MountItemFactory.createIntBufferBatchMountItem( - rootTag, - intBuffer == null ? new int[0] : intBuffer, - objBuffer == null ? new Object[0] : objBuffer, - commitNumber); - } - - /** - * This method enqueues UI operations directly to the UI thread. This might change in the future - * to enforce execution order using {@link ReactChoreographer.CallbackType}. This method should - * only be called as the result of a new tree being committed. - */ - @SuppressLint("NotInvokedPrivateMethod") // Called from C++ via JNI (Binding.cpp) - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void scheduleMountItem( - @Nullable final MountItem mountItem, - int commitNumber, - long commitStartTime, - long diffStartTime, - long diffEndTime, - long layoutStartTime, - long layoutEndTime, - long finishTransactionStartTime, - long finishTransactionEndTime, - int affectedLayoutNodesCount) { - // When Binding.cpp calls scheduleMountItems during a commit phase, it always calls with - // a BatchMountItem. No other sites call into this with a BatchMountItem, and Binding.cpp only - // calls scheduleMountItems with a BatchMountItem. - long scheduleMountItemStartTime = SystemClock.uptimeMillis(); - boolean isBatchMountItem = mountItem instanceof BatchMountItem; - boolean shouldSchedule = false; - if (isBatchMountItem) { - BatchMountItem batchMountItem = (BatchMountItem) mountItem; - Assertions.assertNotNull(batchMountItem, "BatchMountItem is null"); - shouldSchedule = !batchMountItem.isBatchEmpty(); - } else { - shouldSchedule = mountItem != null; - } - // In case of sync rendering, this could be called on the UI thread. Otherwise, - // it should ~always be called on the JS thread. - for (UIManagerListener listener : mListeners) { - listener.didScheduleMountItems(this); - } - - if (isBatchMountItem) { - mCommitStartTime = commitStartTime; - mLayoutTime = layoutEndTime - layoutStartTime; - mFinishTransactionCPPTime = finishTransactionEndTime - finishTransactionStartTime; - mFinishTransactionTime = scheduleMountItemStartTime - finishTransactionStartTime; - mDispatchViewUpdatesTime = SystemClock.uptimeMillis(); - } - - if (shouldSchedule) { - Assertions.assertNotNull(mountItem, "MountItem is null"); - mMountItemDispatcher.addMountItem(mountItem); - if (UiThreadUtil.isOnUiThread()) { - Runnable runnable = - new GuardedRunnable(mReactApplicationContext) { - @Override - public void runGuarded() { - mMountItemDispatcher.tryDispatchMountItems(); - } - }; - runnable.run(); - } - } - - // Post markers outside of lock and after sync mounting finishes its execution - if (isBatchMountItem) { - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_COMMIT_START, null, commitNumber, commitStartTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_FINISH_TRANSACTION_START, - null, - commitNumber, - finishTransactionStartTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_FINISH_TRANSACTION_END, - null, - commitNumber, - finishTransactionEndTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_DIFF_START, null, commitNumber, diffStartTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_DIFF_END, null, commitNumber, diffEndTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_LAYOUT_START, null, commitNumber, layoutStartTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_LAYOUT_END, null, commitNumber, layoutEndTime); - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_LAYOUT_AFFECTED_NODES, - null, - commitNumber, - layoutEndTime, - affectedLayoutNodesCount); - ReactMarker.logFabricMarker(ReactMarkerConstants.FABRIC_COMMIT_END, null, commitNumber); - } - } - - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private void scheduleReactRevisionMerge(int surfaceId) { - if (UiThreadUtil.isOnUiThread()) { - if (mBinding != null) { - mBinding.mergeReactRevision(surfaceId); - } - } else { - mPendingReactRevisionMerges.add(surfaceId); - } - } - - /** - * This method initiates preloading of an image specified by ImageSource. It can later be consumed - * by an ImageView. - */ - @UnstableReactNativeAPI - public void experimental_prefetchResources( - int surfaceId, String componentName, ReadableMapBuffer params) { - mMountItemDispatcher.addMountItem( - new PrefetchResourcesMountItem(surfaceId, componentName, params)); - } - - void setBinding(FabricUIManagerBinding binding) { - mBinding = binding; - } - - /** - * Updates the layout metrics of the root view based on the Measure specs received by parameters. - */ - @Override - @UiThread - @ThreadConfined(UI) - public void updateRootLayoutSpecs( - final int surfaceId, - final int widthMeasureSpec, - final int heightMeasureSpec, - final int offsetX, - final int offsetY) { - - if (ReactNativeFeatureFlags.enableFabricLogs()) { - FLog.d(TAG, "Updating Root Layout Specs for [%d]", surfaceId); - } - - SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId); - - // TODO T83615646: make this a hard-crash in the future. - if (surfaceMountingManager == null) { - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalViewOperationException( - "Cannot updateRootLayoutSpecs on surfaceId that does not exist: " + surfaceId)); - return; - } - - Context context = surfaceMountingManager.getContext(); - boolean isRTL = false; - boolean doLeftAndRightSwapInRTL = false; - if (context != null) { - isRTL = I18nUtil.getInstance().isRTL(context); - doLeftAndRightSwapInRTL = I18nUtil.getInstance().doLeftAndRightSwapInRTL(context); - } - - Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); - mBinding.setConstraints( - surfaceId, - getMinSize(widthMeasureSpec), - getMaxSize(widthMeasureSpec), - getMinSize(heightMeasureSpec), - getMaxSize(heightMeasureSpec), - offsetX, - offsetY, - isRTL, - doLeftAndRightSwapInRTL); - } - - @Override - public @Nullable View resolveView(int reactTag) { - UiThreadUtil.assertOnUiThread(); - - SurfaceMountingManager surfaceManager = mMountingManager.getSurfaceManagerForView(reactTag); - if (surfaceManager == null || surfaceManager.isStopped()) { - return null; - } - return surfaceManager.getView(reactTag); - } - - @Deprecated - @Override - public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap params) { - receiveEvent(View.NO_ID, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED); - } - - @Deprecated - @Override - public void receiveEvent( - int surfaceId, int reactTag, String eventName, @Nullable WritableMap params) { - receiveEvent(surfaceId, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED); - } - - /** - * receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals - * that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before - * emitting. - * - *

{@code customCoalesceKey} is currently unused. - * - * @param surfaceId - * @param reactTag - * @param eventName - * @param canCoalesceEvent - * @param params - * @param eventCategory - * @deprecated Use the overload with eventTimestamp parameter instead. - */ - @Deprecated - public void receiveEvent( - int surfaceId, - int reactTag, - String eventName, - boolean canCoalesceEvent, - @Nullable WritableMap params, - @EventCategoryDef int eventCategory) { - receiveEvent( - surfaceId, - reactTag, - eventName, - canCoalesceEvent, - params, - eventCategory, - false, - SystemClock.uptimeMillis()); - } - - /** - * receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals - * that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before - * emitting. - * - *

{@code customCoalesceKey} is currently unused. - * - * @param surfaceId - * @param reactTag - * @param eventName - * @param canCoalesceEvent - * @param params - * @param eventCategory - * @param experimentalIsSynchronous - * @deprecated Use the overload with eventTimestamp parameter instead. - */ - @Deprecated - @Override - public void receiveEvent( - int surfaceId, - int reactTag, - String eventName, - boolean canCoalesceEvent, - @Nullable WritableMap params, - @EventCategoryDef int eventCategory, - boolean experimentalIsSynchronous) { - receiveEvent( - surfaceId, - reactTag, - eventName, - canCoalesceEvent, - params, - eventCategory, - experimentalIsSynchronous, - SystemClock.uptimeMillis()); - } - - /** - * receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals - * that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before - * emitting. - * - *

{@code customCoalesceKey} is currently unused. - * - * @param surfaceId - * @param reactTag - * @param eventName - * @param canCoalesceEvent - * @param params - * @param eventCategory - * @param experimentalIsSynchronous - * @param eventTimestamp - */ - @Override - public void receiveEvent( - int surfaceId, - int reactTag, - String eventName, - boolean canCoalesceEvent, - @Nullable WritableMap params, - @EventCategoryDef int eventCategory, - boolean experimentalIsSynchronous, - long eventTimestamp) { - - if (ReactBuildConfig.DEBUG && surfaceId == View.NO_ID) { - FLog.d(TAG, "Emitted event without surfaceId: [%d] %s", reactTag, eventName); - } - - if (mDestroyed) { - FLog.e(TAG, "Attempted to receiveEvent after destruction"); - return; - } - - if (experimentalIsSynchronous) { - UiThreadUtil.assertOnUiThread(); - EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(surfaceId, reactTag); - if (eventEmitter != null) { - // add() returns true only if there are no equivalent events already in the set - boolean firstEventForFrame = - mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName)); - if (firstEventForFrame) { - eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp); - } - return; - } - } - - mMountingManager.dispatchEvent( - surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory, eventTimestamp); - } - - @Override - public void onHostResume() { - mDispatchUIFrameCallback.resume(); - } - - @Override - public EventDispatcher getEventDispatcher() { - return mEventDispatcher; - } - - @Override - public void onHostPause() { - mDispatchUIFrameCallback.pause(); - } - - @Override - public void onHostDestroy() {} - - @Override - @Deprecated - @AnyThread - @ThreadConfined(ANY) - public void dispatchCommand( - final int reactTag, final int commandId, @Nullable final ReadableArray commandArgs) { - throw new UnsupportedOperationException( - "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through" - + " Fabric JSI API"); - } - - @Override - @Deprecated - @AnyThread - @ThreadConfined(ANY) - public void dispatchCommand( - final int reactTag, final String commandId, @Nullable final ReadableArray commandArgs) { - throw new UnsupportedOperationException( - "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through" - + " Fabric JSI API"); - } - - @Deprecated - @AnyThread - @ThreadConfined(ANY) - public void dispatchCommand( - final int surfaceId, - final int reactTag, - final int commandId, - @Nullable final ReadableArray commandArgs) { - mMountItemDispatcher.addViewCommandMountItem( - MountItemFactory.createDispatchCommandMountItem( - surfaceId, reactTag, commandId, commandArgs)); - } - - @AnyThread - @ThreadConfined(ANY) - public void dispatchCommand( - final int surfaceId, - final int reactTag, - final String commandId, - @Nullable final ReadableArray commandArgs) { - if (ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { - // For Fabric Interop, we check if the commandId is an integer. If it is, we use the integer - // overload of dispatchCommand. Otherwise, we use the string overload. - // and the events won't be correctly dispatched. - mMountItemDispatcher.addViewCommandMountItem( - createDispatchCommandMountItemForInterop(surfaceId, reactTag, commandId, commandArgs)); - } else { - mMountItemDispatcher.addViewCommandMountItem( - MountItemFactory.createDispatchCommandMountItem( - surfaceId, reactTag, commandId, commandArgs)); - } - } - - @Override - @AnyThread - @ThreadConfined(ANY) - public void sendAccessibilityEvent(int reactTag, int eventType) { - // Can be called from native, not just JS - we need to migrate the native callsites - // before removing this entirely. - mMountItemDispatcher.addMountItem( - MountItemFactory.createSendAccessibilityEventMountItem(View.NO_ID, reactTag, eventType)); - } - - @AnyThread - @ThreadConfined(ANY) - public void sendAccessibilityEventFromJS(int surfaceId, int reactTag, String eventTypeJS) { - int eventType; - if ("focus".equals(eventTypeJS)) { - eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED; - } else if ("windowStateChange".equals(eventTypeJS)) { - eventType = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; - } else if ("click".equals(eventTypeJS)) { - eventType = AccessibilityEvent.TYPE_VIEW_CLICKED; - } else if ("viewHoverEnter".equals(eventTypeJS)) { - eventType = AccessibilityEvent.TYPE_VIEW_HOVER_ENTER; - } else { - throw new IllegalArgumentException( - "sendAccessibilityEventFromJS: invalid eventType " + eventTypeJS); - } - mMountItemDispatcher.addMountItem( - MountItemFactory.createSendAccessibilityEventMountItem(surfaceId, reactTag, eventType)); - } - - /** - * Set the JS responder for the view associated with the tags received as a parameter. - * - * @param reactTag React tag of the first parent of the view that is NOT virtual - * @param initialReactTag React tag of the JS view that initiated the touch operation - * @param blockNativeResponder If native responder should be blocked or not - */ - public void setJSResponder( - final int surfaceId, - final int reactTag, - final int initialReactTag, - final boolean blockNativeResponder) { - mMountItemDispatcher.addMountItem( - new MountItem() { - @Override - public void execute(MountingManager mountingManager) { - SurfaceMountingManager surfaceMountingManager = - mountingManager.getSurfaceManager(surfaceId); - if (surfaceMountingManager != null) { - surfaceMountingManager.setJSResponder( - reactTag, initialReactTag, blockNativeResponder); - } else { - FLog.e( - TAG, "setJSResponder skipped, surface no longer available [" + surfaceId + "]"); - } - } - - @Override - public int getSurfaceId() { - return surfaceId; - } - - @SuppressLint("DefaultLocale") - @Override - public String toString() { - return String.format("SET_JS_RESPONDER [%d] [surface:%d]", reactTag, surfaceId); - } - }); - } - - /** - * Clears the JS Responder specified by {@link #setJSResponder}. After this method is called, all - * the touch events are going to be handled by JS. - */ - public void clearJSResponder() { - mMountItemDispatcher.addMountItem( - new MountItem() { - @Override - public void execute(MountingManager mountingManager) { - mountingManager.clearJSResponder(); - } - - @Override - public int getSurfaceId() { - return View.NO_ID; - } - - @Override - public String toString() { - return "CLEAR_JS_RESPONDER"; - } - }); - } - - @Override - public void profileNextBatch() { - // TODO T31905686: Remove this method and add support for multi-threading performance counters - } - - @Override - @Deprecated - @Nullable - public String resolveCustomDirectEventName(@Nullable String eventName) { - if (eventName == null) { - return null; - } - if (eventName.startsWith("top")) { - return "on" + eventName.substring(3); - } - return eventName; - } - - // Called from Binding.cpp - @AnyThread - public void onAnimationStarted() { - mDriveCxxAnimations = true; - } - - // Called from Binding.cpp - @AnyThread - public void onAllAnimationsComplete() { - mDriveCxxAnimations = false; - } - - @Override - public Map getPerformanceCounters() { - HashMap performanceCounters = new HashMap<>(); - performanceCounters.put("CommitStartTime", mCommitStartTime); - performanceCounters.put("LayoutTime", mLayoutTime); - performanceCounters.put("DispatchViewUpdatesTime", mDispatchViewUpdatesTime); - performanceCounters.put("RunStartTime", mMountItemDispatcher.getRunStartTime()); - performanceCounters.put("BatchedExecutionTime", mMountItemDispatcher.getBatchedExecutionTime()); - performanceCounters.put("FinishFabricTransactionTime", mFinishTransactionTime); - performanceCounters.put("FinishFabricTransactionCPPTime", mFinishTransactionCPPTime); - return performanceCounters; - } - - private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener { - @UiThread - @ThreadConfined(UI) - @Override - public void willMountItems(@Nullable List mountItems) { - for (UIManagerListener listener : mListeners) { - listener.willMountItems(FabricUIManager.this); - } - } - - @UiThread - @ThreadConfined(UI) - @Override - public void didMountItems(@Nullable List mountItems) { - for (UIManagerListener listener : mListeners) { - listener.didMountItems(FabricUIManager.this); - } - - if (mountItems == null || mountItems.isEmpty()) { - return; - } - - // Collect surface IDs for all the mount items - for (MountItem mountItem : mountItems) { - if (mountItem != null - && mountItem.getSurfaceId() != View.NO_ID - && !mSurfaceIdsWithPendingMountNotification.contains(mountItem.getSurfaceId())) { - mSurfaceIdsWithPendingMountNotification.add(mountItem.getSurfaceId()); - } - } - - if (!mMountNotificationScheduled && !mSurfaceIdsWithPendingMountNotification.isEmpty()) { - mMountNotificationScheduled = true; - - // Notify mount when the effects are visible and prevent mount hooks to - // delay paint. - UiThreadUtil.getUiThreadHandler() - .postAtFrontOfQueue( - () -> { - mMountNotificationScheduled = false; - - // Create a copy in case mount hooks trigger more mutations - final List surfaceIdsToReportMount = - mSurfaceIdsWithPendingMountNotification; - mSurfaceIdsWithPendingMountNotification = new ArrayList<>(); - - final @Nullable FabricUIManagerBinding binding = mBinding; - if (binding == null || mDestroyed) { - return; - } - - for (int surfaceId : surfaceIdsToReportMount) { - binding.reportMount(surfaceId); - } - }); - } - } - - @Override - public void didDispatchMountItems() { - for (UIManagerListener listener : mListeners) { - listener.didDispatchMountItems(FabricUIManager.this); - } - } - } - - /** - * Util function that takes care of handling commands for Fabric Interop. If the command is a - * string that represents a number (say "42"), it will be parsed as an integer and the - * corresponding dispatch command mount item will be created. - */ - /* package */ DispatchCommandMountItem createDispatchCommandMountItemForInterop( - final int surfaceId, - final int reactTag, - final String commandId, - @Nullable final ReadableArray commandArgs) { - try { - int commandIdInteger = Integer.parseInt(commandId); - return MountItemFactory.createDispatchCommandMountItem( - surfaceId, reactTag, commandIdInteger, commandArgs); - } catch (NumberFormatException e) { - return MountItemFactory.createDispatchCommandMountItem( - surfaceId, reactTag, commandId, commandArgs); - } - } - - private class DispatchUIFrameCallback extends GuardedFrameCallback { - - private volatile boolean mIsMountingEnabled = true; - - @ThreadConfined(UI) - private boolean mShouldSchedule = false; - - @ThreadConfined(UI) - private boolean mIsScheduled = false; - - private DispatchUIFrameCallback(ReactContext reactContext) { - super(reactContext); - } - - @UiThread - @ThreadConfined(UI) - private void schedule() { - if (!mIsScheduled && mShouldSchedule) { - mIsScheduled = true; - ReactChoreographer.getInstance() - .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this); - } - } - - @UiThread - @ThreadConfined(UI) - void resume() { - mShouldSchedule = true; - schedule(); - } - - @UiThread - @ThreadConfined(UI) - void pause() { - ReactChoreographer.getInstance() - .removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this); - mShouldSchedule = false; - mIsScheduled = false; - } - - @Override - @UiThread - @ThreadConfined(UI) - public void doFrameGuarded(long frameTimeNanos) { - mIsScheduled = false; - - if (!mIsMountingEnabled) { - FLog.w(TAG, "Not flushing pending UI operations: exception was previously thrown"); - return; - } - - if (mDestroyed) { - FLog.w(TAG, "Not flushing pending UI operations: FabricUIManager is destroyed"); - return; - } - - // Drain pending React revision merges first so that animations, - // preallocation, and mount items operate against the latest revision. - if (ReactNativeFeatureFlags.enableFabricCommitBranching()) { - FabricUIManagerBinding binding = mBinding; - if (binding != null) { - Integer mergeSurfaceId; - while ((mergeSurfaceId = mPendingReactRevisionMerges.poll()) != null) { - binding.mergeReactRevision(mergeSurfaceId); - } - } - } - - // Drive any animations from C++. - // There is a race condition here between getting/setting - // `mDriveCxxAnimations` which shouldn't matter; it's safe to call - // the mBinding method, unless mBinding has gone away. - if ((mDriveCxxAnimations || ReactNativeFeatureFlags.cxxNativeAnimatedEnabled()) - && mBinding != null) { - mBinding.driveCxxAnimations(); - } - - if (!ReactNativeFeatureFlags.disableViewPreallocationAndroid() && mBinding != null) { - mBinding.drainPreallocateViewsQueue(); - } - - try { - // First, execute as many pre mount items as we can within frameTimeNanos time. - // If not all pre mount items were executed, following may happen: - // 1. In case there are view commands or mount items in MountItemDispatcher: execute - // remaining pre mount items. - // 2. In case there are no view commands or mount items, wait until next frame. - mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos); - mMountItemDispatcher.tryDispatchMountItems(); - } catch (Exception ex) { - FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex); - mIsMountingEnabled = false; - throw ex; - } finally { - schedule(); - } - - mSynchronousEvents.clear(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.kt new file mode 100644 index 00000000000..ae37489d5fc --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.kt @@ -0,0 +1,1471 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") + +package com.facebook.react.fabric + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Point +import android.os.SystemClock +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.annotation.AnyThread +import androidx.annotation.UiThread +import androidx.core.view.ViewCompat.FocusDirection +import com.facebook.common.logging.FLog +import com.facebook.infer.annotation.ThreadConfined +import com.facebook.proguard.annotations.DoNotStripAny +import com.facebook.react.bridge.ColorPropConverter +import com.facebook.react.bridge.GuardedRunnable +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.NativeArray +import com.facebook.react.bridge.NativeMap +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.ReactMarkerConstants +import com.facebook.react.bridge.ReactSoftExceptionLogger +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.UIManager +import com.facebook.react.bridge.UIManagerListener +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.bridge.WritableMap +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.build.ReactBuildConfig +import com.facebook.react.common.mapbuffer.ReadableMapBuffer +import com.facebook.react.fabric.events.EventEmitterWrapper +import com.facebook.react.fabric.events.FabricEventEmitter +import com.facebook.react.fabric.internal.interop.InteropUIBlockListener +import com.facebook.react.fabric.interop.UIBlock +import com.facebook.react.fabric.interop.UIBlockViewResolver +import com.facebook.react.fabric.mounting.LayoutMetricsConversions +import com.facebook.react.fabric.mounting.MountItemDispatcher +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.fabric.mounting.SurfaceMountingManager +import com.facebook.react.fabric.mounting.mountitems.BatchMountItem +import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem +import com.facebook.react.fabric.mounting.mountitems.MountItem +import com.facebook.react.fabric.mounting.mountitems.MountItemFactory +import com.facebook.react.fabric.mounting.mountitems.PrefetchResourcesMountItem +import com.facebook.react.fabric.mounting.mountitems.SynchronousMountItem +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags +import com.facebook.react.internal.interop.InteropEventEmitter +import com.facebook.react.modules.core.ReactChoreographer +import com.facebook.react.modules.i18nmanager.I18nUtil +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.GuardedFrameCallback +import com.facebook.react.uimanager.IllegalViewOperationException +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.ReactRoot +import com.facebook.react.uimanager.ReactRootViewTagGenerator +import com.facebook.react.uimanager.RootViewUtil +import com.facebook.react.uimanager.StateWrapper +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.UIManagerHelper.PADDING_BOTTOM_INDEX +import com.facebook.react.uimanager.UIManagerHelper.PADDING_END_INDEX +import com.facebook.react.uimanager.UIManagerHelper.PADDING_START_INDEX +import com.facebook.react.uimanager.UIManagerHelper.PADDING_TOP_INDEX +import com.facebook.react.uimanager.ViewManagerPropertyUpdater +import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.uimanager.events.BatchEventDispatchedListener +import com.facebook.react.uimanager.events.EventCategoryDef +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.FabricEventDispatcher +import com.facebook.react.uimanager.events.RCTEventEmitter +import com.facebook.react.uimanager.events.SynchronousEventReceiver +import com.facebook.react.views.text.PreparedLayout +import com.facebook.react.views.text.ReactTextViewManager +import com.facebook.react.views.text.ReactTextViewManagerCallback +import com.facebook.react.views.text.TextEffectRegistry +import com.facebook.react.views.text.TextLayoutManager +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CopyOnWriteArrayList + +/** + * We instruct ProGuard not to strip out any fields or methods, because many of these methods are + * only called through the JNI from Cxx so it appears that most of this class is "unused". + */ +@SuppressLint("MissingNativeLoadLibrary") +@DoNotStripAny +@OptIn(UnstableReactNativeAPI::class) +public open class FabricUIManager( + private val reactApplicationContext: ReactApplicationContext, + private val viewManagerRegistry: ViewManagerRegistry, + batchEventDispatchedListener: BatchEventDispatchedListener, +) : UIManager, LifecycleEventListener, UIBlockViewResolver, SynchronousEventReceiver { + + @JvmField public var devToolsReactPerfLogger: DevToolsReactPerfLogger? = null + + private var uiManagerBinding: FabricUIManagerBinding? = null + private val mountingManager: MountingManager + private val _eventDispatcher: FabricEventDispatcher + private val mountItemDispatcher: MountItemDispatcher + + private val _textEffectRegistry: TextEffectRegistry = TextEffectRegistry() + + private val batchEventDispatchedListener: BatchEventDispatchedListener + + private val listeners: MutableList = CopyOnWriteArrayList() + + private var mountNotificationScheduled: Boolean = false + private var surfaceIdsWithPendingMountNotification: MutableList = ArrayList() + + @ThreadConfined(ThreadConfined.UI) private val dispatchUIFrameCallback: DispatchUIFrameCallback + + @ThreadConfined(ThreadConfined.UI) + private val synchronousEvents: MutableSet = HashSet() + + private val pendingReactRevisionMerges: ConcurrentLinkedQueue = ConcurrentLinkedQueue() + + @Volatile private var destroyed: Boolean = false + + private var driveCxxAnimations: Boolean = false + + private var viewTransitionSnapshotManager: ViewTransitionSnapshotManager? = null + + private var dispatchViewUpdatesTime: Long = 0L + private var commitStartTime: Long = 0L + private var layoutTime: Long = 0L + private var finishTransactionTime: Long = 0L + private var finishTransactionCPPTime: Long = 0L + + private var currentSynchronousCommitNumber: Int = 10000 + + @Suppress("UNCHECKED_CAST") + private val mountItemExecutor: MountingManager.MountItemExecutor = + MountingManager.MountItemExecutor { items -> + mountItemDispatcher.dispatchMountItems(items as java.util.Queue) + } + + private var interopUIBlockListener: InteropUIBlockListener? = null + + init { + dispatchUIFrameCallback = DispatchUIFrameCallback(reactApplicationContext) + mountingManager = MountingManager(viewManagerRegistry, mountItemExecutor) + mountItemDispatcher = MountItemDispatcher(mountingManager, MountItemDispatchListener()) + _eventDispatcher = FabricEventDispatcher(reactApplicationContext, FabricEventEmitter(this)) + this.batchEventDispatchedListener = batchEventDispatchedListener + reactApplicationContext.addLifecycleEventListener(this) + reactApplicationContext.registerComponentCallbacks(viewManagerRegistry) + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + @Deprecated("Do not call addRootView in Fabric; it is unsupported. Call startSurface instead.") + override fun addRootView(rootView: T, initialProps: WritableMap?): Int { + ReactSoftExceptionLogger.logSoftException( + TAG, + IllegalViewOperationException( + "Do not call addRootView in Fabric; it is unsupported. Call startSurface instead." + ), + ) + + val reactRootView = rootView as ReactRoot + val rootTag = reactRootView.getRootViewTag() + + val reactContext = + ThemedReactContext( + reactApplicationContext, + rootView.context, + reactRootView.getSurfaceID(), + rootTag, + ) + mountingManager.startSurface(rootTag, reactContext, rootView) + val moduleName = reactRootView.getJSModuleName() + if (ReactNativeFeatureFlags.enableFabricLogs()) { + FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag) + } + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.startSurface(rootTag, moduleName, initialProps as NativeMap?) + return rootTag + } + + /** + * Find the next focusable element's id and position relative to the parent from the shadow tree + * based on the current focusable element and the direction. + * + * @return A NextFocusableNode object where the 'id' is the reactId/Tag of the next focusable + * view, returns null if no view could be found + */ + public fun findNextFocusableElement( + parentTag: Int, + focusedTag: Int, + @FocusDirection direction: Int, + ): Int? { + val binding = uiManagerBinding ?: return null + + val generalizedDirection: Int = + when (direction) { + View.FOCUS_DOWN -> 0 + View.FOCUS_UP -> 1 + View.FOCUS_RIGHT -> 2 + View.FOCUS_LEFT -> 3 + View.FOCUS_FORWARD -> 4 + View.FOCUS_BACKWARD -> 5 + else -> return null + } + + val serializedNextFocusableNodeMetrics = + binding.findNextFocusableElement(parentTag, focusedTag, generalizedDirection) + + if (serializedNextFocusableNodeMetrics == -1) { + return null + } + + return serializedNextFocusableNodeMetrics + } + + public fun getRelativeAncestorList(rootTag: Int, childTag: Int): IntArray? { + return uiManagerBinding?.getRelativeAncestorList(rootTag, childTag) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun startSurface( + rootView: T, + moduleName: String, + initialProps: WritableMap?, + widthMeasureSpec: Int, + heightMeasureSpec: Int, + ): Int { + val rootTag = (rootView as ReactRoot).getRootViewTag() + val context: Context = (rootView as View).context + val reactContext = ThemedReactContext(reactApplicationContext, context, moduleName, rootTag) + if (ReactNativeFeatureFlags.enableFabricLogs()) { + FLog.d(TAG, "Starting surface for module: %s and reactTag: %d", moduleName, rootTag) + } + mountingManager.startSurface(rootTag, reactContext, rootView as View) + + @SuppressLint("WrongThread") + val viewportOffset = + if (UiThreadUtil.isOnUiThread()) RootViewUtil.getViewportOffset(rootView as View) + else Point(0, 0) + + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.startSurfaceWithConstraints( + rootTag, + moduleName, + initialProps as NativeMap?, + LayoutMetricsConversions.getMinSize(widthMeasureSpec), + LayoutMetricsConversions.getMaxSize(widthMeasureSpec), + LayoutMetricsConversions.getMinSize(heightMeasureSpec), + LayoutMetricsConversions.getMaxSize(heightMeasureSpec), + viewportOffset.x.toFloat(), + viewportOffset.y.toFloat(), + I18nUtil.getInstance().isRTL(context), + I18nUtil.getInstance().doLeftAndRightSwapInRTL(context), + ) + return rootTag + } + + internal fun startSurface( + surfaceHandler: SurfaceHandlerBinding, + context: Context, + rootView: View?, + ) { + val rootTag = + if (rootView is ReactRoot) (rootView as ReactRoot).getRootViewTag() + else ReactRootViewTagGenerator.getNextRootViewTag() + + val reactContext = + ThemedReactContext( + reactApplicationContext, + context, + surfaceHandler.moduleName, + rootTag, + ) + mountingManager.startSurface(rootTag, reactContext, rootView) + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.startSurfaceWithSurfaceHandler(rootTag, surfaceHandler, rootView != null) + } + + internal fun attachRootView(surfaceHandler: SurfaceHandlerBinding, rootView: View) { + val reactContext = + ThemedReactContext( + reactApplicationContext, + rootView.context, + surfaceHandler.moduleName, + surfaceHandler.surfaceId, + ) + mountingManager.attachRootView(surfaceHandler.surfaceId, rootView, reactContext) + + surfaceHandler.setMountable(true) + } + + internal fun stopSurface(surfaceHandler: SurfaceHandlerBinding) { + if (!surfaceHandler.isRunning) { + ReactSoftExceptionLogger.logSoftException( + TAG, + IllegalStateException("Trying to stop surface that hasn't started yet"), + ) + return + } + + mountingManager.stopSurface(surfaceHandler.surfaceId) + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.stopSurfaceWithSurfaceHandler(surfaceHandler) + } + + @Suppress("unused") + public fun onRequestEventBeat() { + _eventDispatcher.dispatchAllEvents() + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun stopSurface(surfaceId: Int) { + mountingManager.stopSurface(surfaceId) + + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.stopSurface(surfaceId) + } + + override fun initialize() { + _eventDispatcher.addBatchEventDispatchedListener(batchEventDispatchedListener) + if (ReactNativeFeatureFlags.enableFabricLogs()) { + val perfLogger = DevToolsReactPerfLogger() + devToolsReactPerfLogger = perfLogger + perfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER) + + ReactMarker.addFabricListener(perfLogger) + } + if ( + !ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP && + ReactNativeNewArchitectureFeatureFlags.useFabricInterop() + ) { + val interopEventEmitter = InteropEventEmitter(reactApplicationContext) + reactApplicationContext.internal_registerInteropModule( + RCTEventEmitter::class.java, + interopEventEmitter, + ) + } + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun invalidate() { + FLog.i(TAG, "FabricUIManager.invalidate") + + val perfLogger = devToolsReactPerfLogger + if (perfLogger != null) { + perfLogger.removeDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER) + ReactMarker.removeFabricListener(perfLogger) + } + + if (destroyed) { + ReactSoftExceptionLogger.logSoftException( + TAG, + IllegalStateException("Cannot double-destroy FabricUIManager"), + ) + return + } + + destroyed = true + + _eventDispatcher.removeBatchEventDispatchedListener(batchEventDispatchedListener) + _eventDispatcher.invalidate() + + reactApplicationContext.unregisterComponentCallbacks(viewManagerRegistry) + viewManagerRegistry.invalidate() + + reactApplicationContext.removeLifecycleEventListener(this) + onHostPause() + + uiManagerBinding?.unregister() + uiManagerBinding = null + + ViewManagerPropertyUpdater.clear() + } + + override fun markActiveTouchForTag(surfaceId: Int, reactTag: Int) { + val surfaceMountingManager: SurfaceMountingManager? = + mountingManager.getSurfaceManager(surfaceId) + surfaceMountingManager?.markActiveTouchForTag(reactTag) + } + + override fun sweepActiveTouchForTag(surfaceId: Int, reactTag: Int) { + val surfaceMountingManager: SurfaceMountingManager? = + mountingManager.getSurfaceManager(surfaceId) + surfaceMountingManager?.sweepActiveTouchForTag(reactTag) + } + + public fun addUIBlock(block: UIBlock) { + if ( + !ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP && + ReactNativeNewArchitectureFeatureFlags.useFabricInterop() + ) { + val listener = getInteropUIBlockListener() + listener.addUIBlock(block) + } + } + + public fun prependUIBlock(block: UIBlock) { + if ( + !ReactBuildConfig.UNSTABLE_REMOVE_LEGACY_COMPONENT_INTEROP && + ReactNativeNewArchitectureFeatureFlags.useFabricInterop() + ) { + val listener = getInteropUIBlockListener() + listener.prependUIBlock(block) + } + } + + private fun getInteropUIBlockListener(): InteropUIBlockListener { + var listener = interopUIBlockListener + if (listener == null) { + listener = InteropUIBlockListener() + interopUIBlockListener = listener + addUIManagerEventListener(listener) + } + return listener + } + + @Suppress("unused") + private fun measureLines( + attributedString: ReadableMapBuffer, + paragraphAttributes: ReadableMapBuffer, + width: Float, + height: Float, + ): NativeArray { + val textViewManager = viewManagerRegistry.get(ReactTextViewManager.REACT_CLASS) + + return TextLayoutManager.measureLines( + reactApplicationContext.assets, + attributedString, + paragraphAttributes, + PixelUtil.toPixelFromDIP(width), + PixelUtil.toPixelFromDIP(height), + if (textViewManager is ReactTextViewManagerCallback) + textViewManager as ReactTextViewManagerCallback + else null, + textEffectRegistry, + ) as NativeArray + } + + public fun getColor(surfaceId: Int, resourcePaths: Array): Int { + val context: ThemedReactContext? = + mountingManager.getSurfaceManagerEnforced(surfaceId, "getColor").context + if (context == null) { + return 0 + } + + for (resourcePath in resourcePaths) { + val color = ColorPropConverter.resolveResourcePath(context, resourcePath) + if (color != null) { + return color + } + } + return 0 + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + public fun measure( + surfaceId: Int, + componentName: String, + localData: ReadableMap, + props: ReadableMap, + state: ReadableMap, + minWidth: Float, + maxWidth: Float, + minHeight: Float, + maxHeight: Float, + ): Long { + val context: ReactContext? + if (surfaceId > 0) { + val surfaceMountingManager = mountingManager.getSurfaceManagerEnforced(surfaceId, "measure") + if (surfaceMountingManager.isStopped) { + return 0 + } + context = surfaceMountingManager.context + checkNotNull(context) { + "Context in SurfaceMountingManager is null. surfaceId: $surfaceId" + } + } else { + context = reactApplicationContext + } + + return mountingManager.measure( + context, + componentName, + localData, + props, + state, + LayoutMetricsConversions.getYogaSize(minWidth, maxWidth), + LayoutMetricsConversions.getYogaMeasureMode(minWidth, maxWidth), + LayoutMetricsConversions.getYogaSize(minHeight, maxHeight), + LayoutMetricsConversions.getYogaMeasureMode(minHeight, maxHeight), + null, + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + @UnstableReactNativeAPI + public fun measureText( + attributedString: ReadableMapBuffer, + paragraphAttributes: ReadableMapBuffer, + minWidth: Float, + maxWidth: Float, + minHeight: Float, + maxHeight: Float, + attachmentsPositions: FloatArray?, + ): Long { + val textViewManager = viewManagerRegistry.get(ReactTextViewManager.REACT_CLASS) + + return TextLayoutManager.measureText( + reactApplicationContext.assets, + attributedString, + paragraphAttributes, + LayoutMetricsConversions.getYogaSize(minWidth, maxWidth), + LayoutMetricsConversions.getYogaMeasureMode(minWidth, maxWidth), + LayoutMetricsConversions.getYogaSize(minHeight, maxHeight), + LayoutMetricsConversions.getYogaMeasureMode(minHeight, maxHeight), + if (textViewManager is ReactTextViewManagerCallback) + textViewManager as ReactTextViewManagerCallback + else null, + attachmentsPositions, + textEffectRegistry, + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + @UnstableReactNativeAPI + @PublishedApi + internal fun prepareTextLayout( + attributedString: ReadableMapBuffer, + paragraphAttributes: ReadableMapBuffer, + minWidth: Float, + maxWidth: Float, + minHeight: Float, + maxHeight: Float, + ): PreparedLayout { + val textViewManager = viewManagerRegistry.get(ReactTextViewManager.REACT_CLASS) + + return TextLayoutManager.createPreparedLayout( + reactApplicationContext.assets, + attributedString, + paragraphAttributes, + LayoutMetricsConversions.getYogaSize(minWidth, maxWidth), + LayoutMetricsConversions.getYogaMeasureMode(minWidth, maxWidth), + LayoutMetricsConversions.getYogaSize(minHeight, maxHeight), + LayoutMetricsConversions.getYogaMeasureMode(minHeight, maxHeight), + if (textViewManager is ReactTextViewManagerCallback) + textViewManager as ReactTextViewManagerCallback + else null, + textEffectRegistry, + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + @UnstableReactNativeAPI + @PublishedApi + internal fun reusePreparedLayoutWithNewReactTags( + preparedLayout: PreparedLayout, + reactTags: IntArray, + ): PreparedLayout { + return PreparedLayout( + preparedLayout.layout, + preparedLayout.maximumNumberOfLines, + preparedLayout.verticalOffset, + reactTags, + preparedLayout.textBreakStrategy, + preparedLayout.justificationMode, + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + @UnstableReactNativeAPI + @PublishedApi + internal fun measurePreparedLayout( + preparedLayout: PreparedLayout, + minWidth: Float, + maxWidth: Float, + minHeight: Float, + maxHeight: Float, + ): FloatArray { + return TextLayoutManager.measurePreparedLayout( + preparedLayout, + LayoutMetricsConversions.getYogaSize(minWidth, maxWidth), + LayoutMetricsConversions.getYogaMeasureMode(minWidth, maxWidth), + LayoutMetricsConversions.getYogaSize(minHeight, maxHeight), + LayoutMetricsConversions.getYogaMeasureMode(minHeight, maxHeight), + ) + } + + @UnstableReactNativeAPI + @get:JvmName("getTextEffectRegistry") + public val textEffectRegistry: TextEffectRegistry + get() = _textEffectRegistry + + @Suppress("unused") + public fun getThemeData(surfaceId: Int, defaultTextInputPadding: FloatArray): Boolean { + val surfaceMountingManager: SurfaceMountingManager? = + mountingManager.getSurfaceManager(surfaceId) + val context: Context? = surfaceMountingManager?.context + if (context == null) { + FLog.w(TAG, "Couldn't get context for surfaceId %d in getThemeData", surfaceId) + return false + } + + val defaultTextInputPaddingForTheme = UIManagerHelper.getDefaultTextInputPadding(context) + defaultTextInputPadding[0] = defaultTextInputPaddingForTheme[PADDING_START_INDEX] + defaultTextInputPadding[1] = defaultTextInputPaddingForTheme[PADDING_END_INDEX] + defaultTextInputPadding[2] = defaultTextInputPaddingForTheme[PADDING_TOP_INDEX] + defaultTextInputPadding[3] = defaultTextInputPaddingForTheme[PADDING_BOTTOM_INDEX] + return true + } + + private fun getEncodedScreenSizeWithoutVerticalInsets(surfaceId: Int): Long { + val context: ThemedReactContext? = + mountingManager + .getSurfaceManagerEnforced(surfaceId, "getEncodedScreenSizeWithoutVerticalInsets") + .context + if (context == null) { + FLog.w(TAG, "Couldn't get context from SurfaceMountingManager for surfaceId %d", surfaceId) + return 0 + } else { + return DisplayMetricsHolder.getEncodedScreenSizeWithoutVerticalInsets(context.currentActivity) + } + } + + override fun addUIManagerEventListener(listener: UIManagerListener) { + listeners.add(listener) + } + + override fun removeUIManagerEventListener(listener: UIManagerListener) { + listeners.remove(listener) + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + override fun synchronouslyUpdateViewOnUIThread(reactTag: Int, props: ReadableMap) { + UiThreadUtil.assertOnUiThread() + + val commitNumber = currentSynchronousCommitNumber++ + + if (!mountingManager.getViewExists(reactTag)) { + mountItemDispatcher.addMountItem(SynchronousMountItem(reactTag, props)) + return + } + + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, + null, + commitNumber, + ) + + if (ReactNativeFeatureFlags.enableFabricLogs()) { + FLog.d( + TAG, + "SynchronouslyUpdateViewOnUIThread for tag %d: %s", + reactTag, + if (IS_DEVELOPMENT_ENVIRONMENT) props.toHashMap().toString() else "", + ) + } + + SynchronousMountItem(reactTag, props).execute(mountingManager) + + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, + null, + commitNumber, + ) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun captureViewSnapshot(reactTag: Int, surfaceId: Int) { + getViewTransitionSnapshotManager().captureViewSnapshot(reactTag, surfaceId) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun setViewSnapshot(sourceTag: Int, targetTag: Int, surfaceId: Int) { + getViewTransitionSnapshotManager().setViewSnapshot(sourceTag, targetTag) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun clearPendingSnapshots() { + getViewTransitionSnapshotManager().clearPendingSnapshots() + } + + @Synchronized + private fun getViewTransitionSnapshotManager(): ViewTransitionSnapshotManager { + var manager = viewTransitionSnapshotManager + if (manager == null) { + manager = ViewTransitionSnapshotManager(this, mountingManager) + viewTransitionSnapshotManager = manager + } + return manager + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun preallocateView( + rootTag: Int, + reactTag: Int, + componentName: String, + props: Any?, + stateWrapper: Any?, + isLayoutable: Boolean, + ) { + mountItemDispatcher.addPreAllocateMountItem( + MountItemFactory.createPreAllocateViewMountItem( + rootTag, + reactTag, + componentName, + props as ReadableMap, + stateWrapper as StateWrapper?, + isLayoutable, + ) + ) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun destroyUnmountedView(surfaceId: Int, reactTag: Int) { + mountItemDispatcher.addMountItem( + MountItemFactory.createDestroyViewMountItem(surfaceId, reactTag) + ) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun isOnMainThread(): Boolean { + return UiThreadUtil.isOnUiThread() + } + + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun createIntBufferBatchMountItem( + rootTag: Int, + intBuffer: IntArray?, + objBuffer: Array?, + commitNumber: Int, + ): MountItem { + return MountItemFactory.createIntBufferBatchMountItem( + rootTag, + intBuffer ?: IntArray(0), + objBuffer ?: emptyArray(), + commitNumber, + ) + } + + @SuppressLint("NotInvokedPrivateMethod") + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun scheduleMountItem( + mountItem: MountItem?, + commitNumber: Int, + commitStartTime: Long, + diffStartTime: Long, + diffEndTime: Long, + layoutStartTime: Long, + layoutEndTime: Long, + finishTransactionStartTime: Long, + finishTransactionEndTime: Long, + affectedLayoutNodesCount: Int, + ) { + val scheduleMountItemStartTime = SystemClock.uptimeMillis() + val isBatchMountItem = mountItem is BatchMountItem + val shouldSchedule: Boolean + if (isBatchMountItem) { + val batchMountItem = mountItem as BatchMountItem + shouldSchedule = !batchMountItem.isBatchEmpty() + } else { + shouldSchedule = mountItem != null + } + for (listener in listeners) { + listener.didScheduleMountItems(this) + } + + if (isBatchMountItem) { + this.commitStartTime = commitStartTime + this.layoutTime = layoutEndTime - layoutStartTime + this.finishTransactionCPPTime = finishTransactionEndTime - finishTransactionStartTime + this.finishTransactionTime = scheduleMountItemStartTime - finishTransactionStartTime + this.dispatchViewUpdatesTime = SystemClock.uptimeMillis() + } + + if (shouldSchedule) { + val item = checkNotNull(mountItem) { "MountItem is null" } + mountItemDispatcher.addMountItem(item) + if (UiThreadUtil.isOnUiThread()) { + val runnable = + object : GuardedRunnable(reactApplicationContext) { + override fun runGuarded() { + mountItemDispatcher.tryDispatchMountItems() + } + } + runnable.run() + } + } + + if (isBatchMountItem) { + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_COMMIT_START, + null, + commitNumber, + commitStartTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_FINISH_TRANSACTION_START, + null, + commitNumber, + finishTransactionStartTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_FINISH_TRANSACTION_END, + null, + commitNumber, + finishTransactionEndTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_DIFF_START, + null, + commitNumber, + diffStartTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_DIFF_END, + null, + commitNumber, + diffEndTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_LAYOUT_START, + null, + commitNumber, + layoutStartTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_LAYOUT_END, + null, + commitNumber, + layoutEndTime, + ) + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_LAYOUT_AFFECTED_NODES, + null, + commitNumber, + layoutEndTime, + affectedLayoutNodesCount, + ) + ReactMarker.logFabricMarker(ReactMarkerConstants.FABRIC_COMMIT_END, null, commitNumber) + } + } + + @Suppress("unused") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + private fun scheduleReactRevisionMerge(surfaceId: Int) { + if (UiThreadUtil.isOnUiThread()) { + uiManagerBinding?.mergeReactRevision(surfaceId) + } else { + pendingReactRevisionMerges.add(surfaceId) + } + } + + @UnstableReactNativeAPI + public fun experimental_prefetchResources( + surfaceId: Int, + componentName: String, + params: ReadableMapBuffer, + ) { + mountItemDispatcher.addMountItem(PrefetchResourcesMountItem(surfaceId, componentName, params)) + } + + internal fun setBinding(binding: FabricUIManagerBinding) { + uiManagerBinding = binding + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + override fun updateRootLayoutSpecs( + rootTag: Int, + widthMeasureSpec: Int, + heightMeasureSpec: Int, + offsetX: Int, + offsetY: Int, + ) { + if (ReactNativeFeatureFlags.enableFabricLogs()) { + FLog.d(TAG, "Updating Root Layout Specs for [%d]", rootTag) + } + + val surfaceMountingManager: SurfaceMountingManager? = mountingManager.getSurfaceManager(rootTag) + + if (surfaceMountingManager == null) { + ReactSoftExceptionLogger.logSoftException( + TAG, + IllegalViewOperationException( + "Cannot updateRootLayoutSpecs on surfaceId that does not exist: $rootTag" + ), + ) + return + } + + val context: Context? = surfaceMountingManager.context + var isRTL = false + var doLeftAndRightSwapInRTL = false + if (context != null) { + isRTL = I18nUtil.getInstance().isRTL(context) + doLeftAndRightSwapInRTL = I18nUtil.getInstance().doLeftAndRightSwapInRTL(context) + } + + val binding = checkNotNull(uiManagerBinding) { "Binding in FabricUIManager is null" } + binding.setConstraints( + rootTag, + LayoutMetricsConversions.getMinSize(widthMeasureSpec), + LayoutMetricsConversions.getMaxSize(widthMeasureSpec), + LayoutMetricsConversions.getMinSize(heightMeasureSpec), + LayoutMetricsConversions.getMaxSize(heightMeasureSpec), + offsetX.toFloat(), + offsetY.toFloat(), + isRTL, + doLeftAndRightSwapInRTL, + ) + } + + override fun resolveView(reactTag: Int): View? { + UiThreadUtil.assertOnUiThread() + + val surfaceManager: SurfaceMountingManager? = mountingManager.getSurfaceManagerForView(reactTag) + if (surfaceManager == null || surfaceManager.isStopped) { + return null + } + return surfaceManager.getView(reactTag) + } + + @Deprecated("Use the overload with surfaceId parameter instead") + override fun receiveEvent(reactTag: Int, eventName: String, event: WritableMap?) { + receiveEvent(View.NO_ID, reactTag, eventName, false, event, EventCategoryDef.UNSPECIFIED) + } + + @Deprecated("Use the overload with canCoalesceEvent parameter instead") + override fun receiveEvent( + surfaceId: Int, + reactTag: Int, + eventName: String, + event: WritableMap?, + ) { + receiveEvent(surfaceId, reactTag, eventName, false, event, EventCategoryDef.UNSPECIFIED) + } + + @Deprecated("Use the overload with eventTimestamp parameter instead.") + public fun receiveEvent( + surfaceId: Int, + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + ) { + receiveEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + false, + SystemClock.uptimeMillis(), + ) + } + + @Deprecated("Use the overload with eventTimestamp parameter instead.") + override fun receiveEvent( + surfaceId: Int, + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + experimentalIsSynchronous: Boolean, + ) { + receiveEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + experimentalIsSynchronous, + SystemClock.uptimeMillis(), + ) + } + + override fun receiveEvent( + surfaceId: Int, + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + experimentalIsSynchronous: Boolean, + eventTimestamp: Long, + ) { + if (ReactBuildConfig.DEBUG && surfaceId == View.NO_ID) { + FLog.d(TAG, "Emitted event without surfaceId: [%d] %s", reactTag, eventName) + } + + if (destroyed) { + FLog.e(TAG, "Attempted to receiveEvent after destruction") + return + } + + if (experimentalIsSynchronous) { + UiThreadUtil.assertOnUiThread() + val eventEmitter: EventEmitterWrapper? = mountingManager.getEventEmitter(surfaceId, reactTag) + if (eventEmitter != null) { + val firstEventForFrame = + synchronousEvents.add(SynchronousEvent(surfaceId, reactTag, eventName)) + if (firstEventForFrame) { + eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp) + } + return + } + } + + mountingManager.dispatchEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + eventTimestamp, + ) + } + + override fun onHostResume() { + dispatchUIFrameCallback.resume() + } + + override val eventDispatcher: EventDispatcher + get() = _eventDispatcher + + override fun onHostPause() { + dispatchUIFrameCallback.pause() + } + + override fun onHostDestroy() {} + + @Deprecated("Fabric dispatchCommand must be called through Fabric JSI API") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun dispatchCommand(reactTag: Int, commandId: Int, commandArgs: ReadableArray?) { + throw UnsupportedOperationException( + "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through" + + " Fabric JSI API" + ) + } + + @Deprecated("Fabric dispatchCommand must be called through Fabric JSI API") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun dispatchCommand(reactTag: Int, commandId: String, commandArgs: ReadableArray?) { + throw UnsupportedOperationException( + "dispatchCommand called without surfaceId - Fabric dispatchCommand must be called through" + + " Fabric JSI API" + ) + } + + @Deprecated("Use the String overload instead") + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + public fun dispatchCommand( + surfaceId: Int, + reactTag: Int, + commandId: Int, + commandArgs: ReadableArray?, + ) { + mountItemDispatcher.addViewCommandMountItem( + MountItemFactory.createDispatchCommandMountItem( + surfaceId, + reactTag, + commandId, + commandArgs ?: JavaOnlyArray(), + ) + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + public fun dispatchCommand( + surfaceId: Int, + reactTag: Int, + commandId: String, + commandArgs: ReadableArray?, + ) { + val args = commandArgs ?: JavaOnlyArray() + if (ReactNativeNewArchitectureFeatureFlags.useFabricInterop()) { + mountItemDispatcher.addViewCommandMountItem( + createDispatchCommandMountItemForInterop(surfaceId, reactTag, commandId, args) + ) + } else { + mountItemDispatcher.addViewCommandMountItem( + MountItemFactory.createDispatchCommandMountItem( + surfaceId, + reactTag, + commandId, + args, + ) + ) + } + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + override fun sendAccessibilityEvent(reactTag: Int, eventType: Int) { + mountItemDispatcher.addMountItem( + MountItemFactory.createSendAccessibilityEventMountItem(View.NO_ID, reactTag, eventType) + ) + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + public fun sendAccessibilityEventFromJS(surfaceId: Int, reactTag: Int, eventTypeJS: String) { + val eventType: Int = + when (eventTypeJS) { + "focus" -> AccessibilityEvent.TYPE_VIEW_FOCUSED + "windowStateChange" -> AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + "click" -> AccessibilityEvent.TYPE_VIEW_CLICKED + "viewHoverEnter" -> AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + else -> + throw IllegalArgumentException( + "sendAccessibilityEventFromJS: invalid eventType $eventTypeJS" + ) + } + mountItemDispatcher.addMountItem( + MountItemFactory.createSendAccessibilityEventMountItem(surfaceId, reactTag, eventType) + ) + } + + public fun setJSResponder( + surfaceId: Int, + reactTag: Int, + initialReactTag: Int, + blockNativeResponder: Boolean, + ) { + mountItemDispatcher.addMountItem( + object : MountItem { + override fun execute(mountingManager: MountingManager) { + val surfaceMountingManager: SurfaceMountingManager? = + mountingManager.getSurfaceManager(surfaceId) + if (surfaceMountingManager != null) { + surfaceMountingManager.setJSResponder( + reactTag, + initialReactTag, + blockNativeResponder, + ) + } else { + FLog.e( + TAG, + "setJSResponder skipped, surface no longer available [$surfaceId]", + ) + } + } + + override fun getSurfaceId(): Int = surfaceId + + override fun toString(): String = "SET_JS_RESPONDER [$reactTag] [surface:$surfaceId]" + } + ) + } + + public fun clearJSResponder() { + mountItemDispatcher.addMountItem( + object : MountItem { + override fun execute(mountingManager: MountingManager) { + mountingManager.clearJSResponder() + } + + override fun getSurfaceId(): Int = View.NO_ID + + override fun toString(): String = "CLEAR_JS_RESPONDER" + } + ) + } + + override fun profileNextBatch() { + // TODO T31905686: Remove this method and add support for multi-threading performance counters + } + + @Deprecated("Use resolveCustomDirectEventName instead") + override fun resolveCustomDirectEventName(eventName: String): String? { + if (eventName.startsWith("top")) { + return "on" + eventName.substring(3) + } + return eventName + } + + @AnyThread + public fun onAnimationStarted() { + driveCxxAnimations = true + } + + @AnyThread + public fun onAllAnimationsComplete() { + driveCxxAnimations = false + } + + override val performanceCounters: Map + get() = + mapOf( + "CommitStartTime" to commitStartTime, + "LayoutTime" to layoutTime, + "DispatchViewUpdatesTime" to dispatchViewUpdatesTime, + "RunStartTime" to mountItemDispatcher.runStartTime, + "BatchedExecutionTime" to mountItemDispatcher.batchedExecutionTime, + "FinishFabricTransactionTime" to finishTransactionTime, + "FinishFabricTransactionCPPTime" to finishTransactionCPPTime, + ) + + private inner class MountItemDispatchListener : MountItemDispatcher.ItemDispatchListener { + @UiThread + @ThreadConfined(ThreadConfined.UI) + override fun willMountItems(mountItems: List?) { + for (listener in listeners) { + listener.willMountItems(this@FabricUIManager) + } + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + override fun didMountItems(mountItems: List?) { + for (listener in listeners) { + listener.didMountItems(this@FabricUIManager) + } + + if (mountItems == null || mountItems.isEmpty()) { + return + } + + for (mountItem in mountItems) { + val sid = mountItem.getSurfaceId() + if (sid != View.NO_ID && !surfaceIdsWithPendingMountNotification.contains(sid)) { + surfaceIdsWithPendingMountNotification.add(sid) + } + } + + if (!mountNotificationScheduled && surfaceIdsWithPendingMountNotification.isNotEmpty()) { + mountNotificationScheduled = true + + UiThreadUtil.getUiThreadHandler().postAtFrontOfQueue { + mountNotificationScheduled = false + + val surfaceIdsToReportMount = surfaceIdsWithPendingMountNotification + surfaceIdsWithPendingMountNotification = ArrayList() + + val binding: FabricUIManagerBinding? = uiManagerBinding + if (binding == null || destroyed) { + return@postAtFrontOfQueue + } + + for (surfaceId in surfaceIdsToReportMount) { + binding.reportMount(surfaceId) + } + } + } + } + + override fun didDispatchMountItems() { + for (listener in listeners) { + listener.didDispatchMountItems(this@FabricUIManager) + } + } + } + + internal fun createDispatchCommandMountItemForInterop( + surfaceId: Int, + reactTag: Int, + commandId: String, + commandArgs: ReadableArray, + ): DispatchCommandMountItem { + return try { + val commandIdInteger = commandId.toInt() + MountItemFactory.createDispatchCommandMountItem( + surfaceId, + reactTag, + commandIdInteger, + commandArgs, + ) + } catch (e: NumberFormatException) { + MountItemFactory.createDispatchCommandMountItem(surfaceId, reactTag, commandId, commandArgs) + } + } + + private inner class DispatchUIFrameCallback(reactContext: ReactContext) : + GuardedFrameCallback(reactContext) { + + @Volatile private var isMountingEnabled: Boolean = true + + @ThreadConfined(ThreadConfined.UI) private var shouldSchedule: Boolean = false + + @ThreadConfined(ThreadConfined.UI) private var isScheduled: Boolean = false + + @UiThread + @ThreadConfined(ThreadConfined.UI) + private fun schedule() { + if (!isScheduled && shouldSchedule) { + isScheduled = true + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this) + } + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + fun resume() { + shouldSchedule = true + schedule() + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + fun pause() { + ReactChoreographer.getInstance() + .removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this) + shouldSchedule = false + isScheduled = false + } + + @UiThread + @ThreadConfined(ThreadConfined.UI) + override fun doFrameGuarded(frameTimeNanos: Long) { + isScheduled = false + + if (!isMountingEnabled) { + FLog.w(TAG, "Not flushing pending UI operations: exception was previously thrown") + return + } + + if (destroyed) { + FLog.w(TAG, "Not flushing pending UI operations: FabricUIManager is destroyed") + return + } + + if (ReactNativeFeatureFlags.enableFabricCommitBranching()) { + val binding = uiManagerBinding + if (binding != null) { + var mergeSurfaceId: Int? = pendingReactRevisionMerges.poll() + while (mergeSurfaceId != null) { + binding.mergeReactRevision(mergeSurfaceId) + mergeSurfaceId = pendingReactRevisionMerges.poll() + } + } + } + + if ( + (driveCxxAnimations || ReactNativeFeatureFlags.cxxNativeAnimatedEnabled()) && + uiManagerBinding != null + ) { + uiManagerBinding?.driveCxxAnimations() + } + + if (!ReactNativeFeatureFlags.disableViewPreallocationAndroid() && uiManagerBinding != null) { + uiManagerBinding?.drainPreallocateViewsQueue() + } + + try { + mountItemDispatcher.dispatchPreMountItems(frameTimeNanos) + mountItemDispatcher.tryDispatchMountItems() + } catch (ex: Exception) { + FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex) + isMountingEnabled = false + throw ex + } finally { + schedule() + } + + synchronousEvents.clear() + } + } + + public companion object { + @JvmField public val TAG: String = FabricUIManager::class.java.simpleName + + @SuppressLint("ClownyBooleanExpression") + @JvmField + public val IS_DEVELOPMENT_ENVIRONMENT: Boolean = false && ReactBuildConfig.DEBUG + + private val FABRIC_PERF_LOGGER: DevToolsReactPerfLogger.DevToolsReactPerfLoggerListener = + DevToolsReactPerfLogger.DevToolsReactPerfLoggerListener { commitPoint -> + val commitDuration = commitPoint.commitDuration + val layoutDuration = commitPoint.layoutDuration + val diffDuration = commitPoint.diffDuration + val transactionEndDuration = commitPoint.transactionEndDuration + val batchExecutionDuration = commitPoint.batchExecutionDuration + + DevToolsReactPerfLogger.streamingCommitStats.add(commitDuration) + DevToolsReactPerfLogger.streamingLayoutStats.add(layoutDuration) + DevToolsReactPerfLogger.streamingDiffStats.add(diffDuration) + DevToolsReactPerfLogger.streamingTransactionEndStats.add(transactionEndDuration) + DevToolsReactPerfLogger.streamingBatchExecutionStats.add(batchExecutionDuration) + + FLog.i( + TAG, + "Statistics of Fabric commit #%d:\n" + + " - Total commit time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" + + " - Layout time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" + + " - Diffing time: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n" + + " - FinishTransaction (Diffing + JNI serialization): %d ms. Avg: %.2f. Median:" + + " %.2f ms. Max: %d ms.\n" + + " - Mounting: %d ms. Avg: %.2f. Median: %.2f ms. Max: %d ms.\n", + commitPoint.commitNumber, + commitDuration, + DevToolsReactPerfLogger.streamingCommitStats.average, + DevToolsReactPerfLogger.streamingCommitStats.median, + DevToolsReactPerfLogger.streamingCommitStats.max, + layoutDuration, + DevToolsReactPerfLogger.streamingLayoutStats.average, + DevToolsReactPerfLogger.streamingLayoutStats.median, + DevToolsReactPerfLogger.streamingLayoutStats.max, + diffDuration, + DevToolsReactPerfLogger.streamingDiffStats.average, + DevToolsReactPerfLogger.streamingDiffStats.median, + DevToolsReactPerfLogger.streamingDiffStats.max, + transactionEndDuration, + DevToolsReactPerfLogger.streamingTransactionEndStats.average, + DevToolsReactPerfLogger.streamingTransactionEndStats.median, + DevToolsReactPerfLogger.streamingTransactionEndStats.max, + batchExecutionDuration, + DevToolsReactPerfLogger.streamingBatchExecutionStats.average, + DevToolsReactPerfLogger.streamingBatchExecutionStats.median, + DevToolsReactPerfLogger.streamingBatchExecutionStats.max, + ) + } + + init { + FabricSoLoader.staticInit() + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt index 108c628ce15..725ba8250fc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt @@ -9,7 +9,7 @@ package com.facebook.react.fabric.mounting.mountitems import android.view.View import com.facebook.react.bridge.ReadableMap -import com.facebook.react.fabric.FabricUIManager.IS_DEVELOPMENT_ENVIRONMENT +import com.facebook.react.fabric.FabricUIManager.Companion.IS_DEVELOPMENT_ENVIRONMENT import com.facebook.react.fabric.mounting.MountingManager internal class SynchronousMountItem(private val reactTag: Int, private val props: ReadableMap) :