From f33fdca87679d5cc628a2e9dccada728cbb0335b Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 9 Jun 2025 08:44:02 -0700 Subject: [PATCH] Convert and internalize MountingManager (#51872) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51872 This converts to Kotlin and internaline MountingManager. The only usage in OSS is react-natve-live-markdown: https://github.com/Expensify/react-native-live-markdown/issues/693 They're using reflection to access Mounting Manager, which they shouldn't. Other than them, I wasn't able to find meaningful usages of `MountingManager` Changelog: [Android] [Breaking] - Convert to Kotlin and internalize MountingManager Reviewed By: rshest Differential Revision: D76126338 fbshipit-source-id: 5ab491f86d697a82b8e5b02b031877020dfa3e9e --- .../ReactAndroid/api/ReactAndroid.api | 27 -- .../fabric/mounting/MountingManager.java | 431 ------------------ .../react/fabric/mounting/MountingManager.kt | 373 +++++++++++++++ 3 files changed, 373 insertions(+), 458 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 605ca8bd2db..2dffea184fd 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2389,33 +2389,6 @@ public abstract interface class com/facebook/react/fabric/mounting/MountItemDisp public abstract fun willMountItems (Ljava/util/List;)V } -public class com/facebook/react/fabric/mounting/MountingManager { - public static final field TAG Ljava/lang/String; - public fun (Lcom/facebook/react/uimanager/ViewManagerRegistry;Lcom/facebook/react/fabric/mounting/MountingManager$MountItemExecutor;)V - public fun attachRootView (ILandroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V - public fun clearJSResponder ()V - public fun enqueuePendingEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V - public fun getEventEmitter (II)Lcom/facebook/react/fabric/events/EventEmitterWrapper; - public fun getSurfaceManager (I)Lcom/facebook/react/fabric/mounting/SurfaceMountingManager; - public fun getSurfaceManagerEnforced (ILjava/lang/String;)Lcom/facebook/react/fabric/mounting/SurfaceMountingManager; - public fun getSurfaceManagerForView (I)Lcom/facebook/react/fabric/mounting/SurfaceMountingManager; - public fun getSurfaceManagerForViewEnforced (I)Lcom/facebook/react/fabric/mounting/SurfaceMountingManager; - public fun getViewExists (I)Z - public fun isWaitingForViewAttach (I)Z - public fun measure (Lcom/facebook/react/bridge/ReactContext;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;[F)J - public fun receiveCommand (IIILcom/facebook/react/bridge/ReadableArray;)V - public fun receiveCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V - public fun sendAccessibilityEvent (III)V - public fun startSurface (ILcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Lcom/facebook/react/fabric/mounting/SurfaceMountingManager; - public fun stopSurface (I)V - public fun surfaceIsStopped (I)Z - public fun updateProps (ILcom/facebook/react/bridge/ReadableMap;)V -} - -public abstract interface class com/facebook/react/fabric/mounting/MountingManager$MountItemExecutor { - public abstract fun executeItems (Ljava/util/Queue;)V -} - public class com/facebook/react/fabric/mounting/SurfaceMountingManager { public static final field TAG Ljava/lang/String; public fun (ILcom/facebook/react/touch/JSResponderHandler;Lcom/facebook/react/uimanager/ViewManagerRegistry;Lcom/facebook/react/uimanager/RootViewManager;Lcom/facebook/react/fabric/mounting/MountingManager$MountItemExecutor;Lcom/facebook/react/uimanager/ThemedReactContext;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java deleted file mode 100644 index 1c96a16a421..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ /dev/null @@ -1,431 +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.mounting; - -import static com.facebook.infer.annotation.ThreadConfined.ANY; -import static com.facebook.infer.annotation.ThreadConfined.UI; - -import android.view.View; -import androidx.annotation.AnyThread; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -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.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.RetryableMountingLayerException; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.common.annotations.UnstableReactNativeAPI; -import com.facebook.react.common.mapbuffer.MapBuffer; -import com.facebook.react.fabric.FabricUIManager; -import com.facebook.react.fabric.events.EventEmitterWrapper; -import com.facebook.react.fabric.mounting.mountitems.MountItem; -import com.facebook.react.touch.JSResponderHandler; -import com.facebook.react.uimanager.RootViewManager; -import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.ViewManagerRegistry; -import com.facebook.react.uimanager.common.ViewUtil; -import com.facebook.react.uimanager.events.EventCategoryDef; -import com.facebook.yoga.YogaMeasureMode; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Class responsible for actually dispatching view updates enqueued via {@link - * FabricUIManager#scheduleMountItem} on the UI thread. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class MountingManager { - public static final String TAG = MountingManager.class.getSimpleName(); - private static final int MAX_STOPPED_SURFACE_IDS_LENGTH = 15; - - private final ConcurrentHashMap mSurfaceIdToManager = - new ConcurrentHashMap<>(); // any thread - - private final CopyOnWriteArrayList mStoppedSurfaceIds = new CopyOnWriteArrayList<>(); - - @Nullable private SurfaceMountingManager mMostRecentSurfaceMountingManager; - @Nullable private SurfaceMountingManager mLastQueriedSurfaceMountingManager; - - private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); - private final ViewManagerRegistry mViewManagerRegistry; - private final MountItemExecutor mMountItemExecutor; - private final RootViewManager mRootViewManager = new RootViewManager(); - - public interface MountItemExecutor { - @UiThread - @ThreadConfined(UI) - void executeItems(Queue items); - } - - public MountingManager( - ViewManagerRegistry viewManagerRegistry, MountItemExecutor mountItemExecutor) { - mViewManagerRegistry = viewManagerRegistry; - mMountItemExecutor = mountItemExecutor; - } - - /** - * Starts surface without attaching the view. All view operations executed against that surface - * will be queued until the view is attached. - */ - @AnyThread - public SurfaceMountingManager startSurface( - final int surfaceId, ThemedReactContext reactContext, @Nullable View rootView) { - SurfaceMountingManager surfaceMountingManager = - new SurfaceMountingManager( - surfaceId, - mJSResponderHandler, - mViewManagerRegistry, - mRootViewManager, - mMountItemExecutor, - reactContext); - - // There could technically be a race condition here if addRootView is called twice from - // different threads, though this is (probably) extremely unlikely, and likely an error. - // This logic to protect against race conditions is a holdover from older code, and we don't - // know if it actually happens in practice - so, we're logging soft exceptions for now. - // This *will* crash in Debug mode, but not in production. - mSurfaceIdToManager.putIfAbsent(surfaceId, surfaceMountingManager); - if (mSurfaceIdToManager.get(surfaceId) != surfaceMountingManager) { - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalStateException( - "Called startSurface more than once for the SurfaceId [" + surfaceId + "]")); - } - - mMostRecentSurfaceMountingManager = mSurfaceIdToManager.get(surfaceId); - - if (rootView != null) { - surfaceMountingManager.attachRootView(rootView, reactContext); - } - - return surfaceMountingManager; - } - - @AnyThread - public void attachRootView( - final int surfaceId, final View rootView, ThemedReactContext themedReactContext) { - SurfaceMountingManager surfaceMountingManager = - getSurfaceManagerEnforced(surfaceId, "attachView"); - - if (surfaceMountingManager.isStopped()) { - ReactSoftExceptionLogger.logSoftException( - TAG, new IllegalStateException("Trying to attach a view to a stopped surface")); - return; - } - - surfaceMountingManager.attachRootView(rootView, themedReactContext); - } - - @AnyThread - public void stopSurface(final int surfaceId) { - SurfaceMountingManager surfaceMountingManager = mSurfaceIdToManager.get(surfaceId); - if (surfaceMountingManager != null) { - // Maximum number of stopped surfaces to keep track of - while (mStoppedSurfaceIds.size() >= MAX_STOPPED_SURFACE_IDS_LENGTH) { - Integer staleStoppedId = mStoppedSurfaceIds.get(0); - Assertions.assertNotNull(staleStoppedId); - mSurfaceIdToManager.remove(staleStoppedId.intValue()); - mStoppedSurfaceIds.remove(staleStoppedId); - FLog.d(TAG, "Removing stale SurfaceMountingManager: [%d]", staleStoppedId.intValue()); - } - mStoppedSurfaceIds.add(surfaceId); - - surfaceMountingManager.stopSurface(); - - if (mMostRecentSurfaceMountingManager == surfaceMountingManager) { - mMostRecentSurfaceMountingManager = null; - } - if (mLastQueriedSurfaceMountingManager == surfaceMountingManager) { - mLastQueriedSurfaceMountingManager = null; - } - } else { - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalStateException( - "Cannot call stopSurface on non-existent surface: [" + surfaceId + "]")); - } - } - - @Nullable - public SurfaceMountingManager getSurfaceManager(int surfaceId) { - if (mLastQueriedSurfaceMountingManager != null - && mLastQueriedSurfaceMountingManager.getSurfaceId() == surfaceId) { - return mLastQueriedSurfaceMountingManager; - } - - if (mMostRecentSurfaceMountingManager != null - && mMostRecentSurfaceMountingManager.getSurfaceId() == surfaceId) { - return mMostRecentSurfaceMountingManager; - } - - SurfaceMountingManager surfaceMountingManager = mSurfaceIdToManager.get(surfaceId); - mLastQueriedSurfaceMountingManager = surfaceMountingManager; - return surfaceMountingManager; - } - - public SurfaceMountingManager getSurfaceManagerEnforced(int surfaceId, String context) { - SurfaceMountingManager surfaceMountingManager = getSurfaceManager(surfaceId); - - if (surfaceMountingManager == null) { - throw new RetryableMountingLayerException( - "Unable to find SurfaceMountingManager for surfaceId: [" - + surfaceId - + "]. Context: " - + context); - } - - return surfaceMountingManager; - } - - public boolean surfaceIsStopped(int surfaceId) { - if (mStoppedSurfaceIds.contains(surfaceId)) { - return true; - } - - SurfaceMountingManager surfaceMountingManager = getSurfaceManager(surfaceId); - if (surfaceMountingManager != null && surfaceMountingManager.isStopped()) { - return true; - } - - return false; - } - - public boolean isWaitingForViewAttach(int surfaceId) { - SurfaceMountingManager mountingManager = getSurfaceManager(surfaceId); - if (mountingManager == null) { - return false; - } - - if (mountingManager.isStopped()) { - return false; - } - - return !mountingManager.isRootViewAttached(); - } - - /** - * Get SurfaceMountingManager associated with a ReactTag. Unfortunately, this requires lookups - * over N maps, where N is the number of active or recently-stopped Surfaces. Each lookup will - * cost `log(M)` operations where M is the number of reactTags in the surface, so the total cost - * per lookup is `O(N * log(M))`. - * - *

To mitigate this cost, we attempt to keep track of the "most recent" SurfaceMountingManager - * and do lookups in it first. For the vast majority of use-cases, except for events or operations - * sent to off-screen surfaces, or use-cases where multiple surfaces are visible and interactable, - * this will reduce the lookup time to `O(log(M))`. Someone smarter than me could probably figure - * out an amortized time. - * - * @param reactTag - * @return - */ - @Nullable - public SurfaceMountingManager getSurfaceManagerForView(int reactTag) { - if (mMostRecentSurfaceMountingManager != null - && mMostRecentSurfaceMountingManager.getViewExists(reactTag)) { - return mMostRecentSurfaceMountingManager; - } - - for (Map.Entry entry : mSurfaceIdToManager.entrySet()) { - SurfaceMountingManager smm = entry.getValue(); - if (smm != mMostRecentSurfaceMountingManager && smm.getViewExists(reactTag)) { - if (mMostRecentSurfaceMountingManager == null) { - mMostRecentSurfaceMountingManager = smm; - } - return smm; - } - } - return null; - } - - @AnyThread - public SurfaceMountingManager getSurfaceManagerForViewEnforced(int reactTag) { - SurfaceMountingManager surfaceMountingManager = getSurfaceManagerForView(reactTag); - - if (surfaceMountingManager == null) { - throw new RetryableMountingLayerException( - "Unable to find SurfaceMountingManager for tag: [" + reactTag + "]"); - } - - return surfaceMountingManager; - } - - public boolean getViewExists(int reactTag) { - return getSurfaceManagerForView(reactTag) != null; - } - - @Deprecated - public void receiveCommand( - int surfaceId, int reactTag, int commandId, ReadableArray commandArgs) { - UiThreadUtil.assertOnUiThread(); - getSurfaceManagerEnforced(surfaceId, "receiveCommand:int") - .receiveCommand(reactTag, commandId, commandArgs); - } - - public void receiveCommand( - int surfaceId, int reactTag, String commandId, ReadableArray commandArgs) { - UiThreadUtil.assertOnUiThread(); - getSurfaceManagerEnforced(surfaceId, "receiveCommand:string") - .receiveCommand(reactTag, commandId, commandArgs); - } - - /** - * Send an accessibility eventType to a Native View. eventType is any valid `AccessibilityEvent.X` - * value. - * - *

Why accept {@ViewUtil.NO_SURFACE_ID}(-1) SurfaceId? Currently there are calls to - * UIManager.sendAccessibilityEvent which is a legacy API and accepts only reactTag. We will have - * to investigate and migrate away from those calls over time. - * - * @param surfaceId {@link int} that identifies the surface or {@ViewUtil.NO_SURFACE_ID}(-1) to - * temporarily support backward compatibility. - * @param reactTag {@link int} that identifies the react Tag of the view. - * @param eventType {@link int} that identifies Android eventType. see {@link - * View#sendAccessibilityEvent} - */ - public void sendAccessibilityEvent(int surfaceId, int reactTag, int eventType) { - UiThreadUtil.assertOnUiThread(); - if (surfaceId == View.NO_ID) { - getSurfaceManagerForViewEnforced(reactTag).sendAccessibilityEvent(reactTag, eventType); - } else { - getSurfaceManagerEnforced(surfaceId, "sendAccessibilityEvent") - .sendAccessibilityEvent(reactTag, eventType); - } - } - - @UiThread - public void updateProps(int reactTag, @Nullable ReadableMap props) { - UiThreadUtil.assertOnUiThread(); - if (props == null) { - return; - } - - getSurfaceManagerForViewEnforced(reactTag).updateProps(reactTag, props); - } - - /** - * Clears the JS Responder specified by {@link SurfaceMountingManager#setJSResponder}. After this - * method is called, all the touch events are going to be handled by JS. - */ - @UiThread - public void clearJSResponder() { - // MountingManager and SurfaceMountingManagers all share the same JSResponderHandler. - // Must be called on MountingManager instead of SurfaceMountingManager, because we don't - // know what surfaceId it's being called for. - mJSResponderHandler.clearJSResponder(); - } - - @AnyThread - @ThreadConfined(ANY) - public @Nullable EventEmitterWrapper getEventEmitter(int surfaceId, int reactTag) { - SurfaceMountingManager smm = getSurfaceMountingManager(surfaceId, reactTag); - if (smm == null) { - return null; - } - return smm.getEventEmitter(reactTag); - } - - /** - * Measure a component, given localData, props, state, and measurement information. This needs to - * remain here for now - and not in SurfaceMountingManager - because sometimes measures are made - * outside of the context of a Surface; especially from C++ before StartSurface is called. - * - * @param context - * @param componentName - * @param localData - * @param props - * @param state - * @param width - * @param widthMode - * @param height - * @param heightMode - * @param attachmentsPositions - * @return - */ - @AnyThread - public long measure( - ReactContext context, - String componentName, - ReadableMap localData, - ReadableMap props, - ReadableMap state, - float width, - YogaMeasureMode widthMode, - float height, - YogaMeasureMode heightMode, - @Nullable float[] attachmentsPositions) { - - return mViewManagerRegistry - .get(componentName) - .measure( - context, - localData, - props, - state, - width, - widthMode, - height, - heightMode, - attachmentsPositions); - } - - /** - * THIS PREFETCH METHOD IS EXPERIMENTAL, DO NOT USE IT FOR PRODUCTION CODE. IT WILL MOST LIKELY - * CHANGE OR BE REMOVED IN THE FUTURE. - * - * @param reactContext - * @param componentName - * @param surfaceId {@link int} surface ID - * @param reactTag reactTag that should be set as ID of the view instance - * @param params {@link MapBuffer} prefetch request params defined in C++ - */ - @AnyThread - @UnstableReactNativeAPI - public void experimental_prefetchResource( - ReactContext reactContext, - String componentName, - int surfaceId, - int reactTag, - MapBuffer params) { - mViewManagerRegistry - .get(componentName) - .experimental_prefetchResource(reactContext, surfaceId, reactTag, params); - } - - public void enqueuePendingEvent( - int surfaceId, - int reactTag, - String eventName, - boolean canCoalesceEvent, - @Nullable WritableMap params, - @EventCategoryDef int eventCategory) { - SurfaceMountingManager smm = getSurfaceMountingManager(surfaceId, reactTag); - if (smm == null) { - FLog.d( - TAG, - "Cannot queue event without valid surface mounting manager for tag: %d, surfaceId: %d", - reactTag, - surfaceId); - return; - } - smm.enqueuePendingEvent(reactTag, eventName, canCoalesceEvent, params, eventCategory); - } - - private @Nullable SurfaceMountingManager getSurfaceMountingManager(int surfaceId, int reactTag) { - return (surfaceId == ViewUtil.NO_SURFACE_ID - ? getSurfaceManagerForView(reactTag) - : getSurfaceManager(surfaceId)); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt new file mode 100644 index 00000000000..0f99db7b027 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt @@ -0,0 +1,373 @@ +/* + * 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.mounting + +import android.view.View +import androidx.annotation.AnyThread +import androidx.annotation.UiThread +import com.facebook.common.logging.FLog +import com.facebook.infer.annotation.ThreadConfined +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.RetryableMountingLayerException +import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread +import com.facebook.react.bridge.WritableMap +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.mapbuffer.MapBuffer +import com.facebook.react.fabric.events.EventEmitterWrapper +import com.facebook.react.fabric.mounting.mountitems.MountItem +import com.facebook.react.touch.JSResponderHandler +import com.facebook.react.uimanager.RootViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.events.EventCategoryDef +import com.facebook.yoga.YogaMeasureMode +import java.util.Queue +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Class responsible for actually dispatching view updates enqueued via + * [FabricUIManager.scheduleMountItem] on the UI thread. + */ +internal class MountingManager( + private val viewManagerRegistry: ViewManagerRegistry, + private val mountItemExecutor: MountItemExecutor +) { + private val surfaceIdToManager = ConcurrentHashMap() // any thread + + private val stoppedSurfaceIds = CopyOnWriteArrayList() + + private var mostRecentSurfaceMountingManager: SurfaceMountingManager? = null + private var lastQueriedSurfaceMountingManager: SurfaceMountingManager? = null + + private val jsResponderHandler = JSResponderHandler() + private val rootViewManager = RootViewManager() + + internal fun interface MountItemExecutor { + @UiThread @ThreadConfined(ThreadConfined.UI) fun executeItems(items: Queue?) + } + + /** + * Starts surface without attaching the view. All view operations executed against that surface + * will be queued until the view is attached. + */ + @AnyThread + fun startSurface( + surfaceId: Int, + reactContext: ThemedReactContext?, + rootView: View? + ): SurfaceMountingManager { + val surfaceMountingManager = + SurfaceMountingManager( + surfaceId, + jsResponderHandler, + viewManagerRegistry, + rootViewManager, + mountItemExecutor, + checkNotNull(reactContext)) + + // There could technically be a race condition here if addRootView is called twice from + // different threads, though this is (probably) extremely unlikely, and likely an error. + // This logic to protect against race conditions is a holdover from older code, and we don't + // know if it actually happens in practice - so, we're logging soft exceptions for now. + // This *will* crash in Debug mode, but not in production. + surfaceIdToManager.putIfAbsent(surfaceId, surfaceMountingManager) + if (surfaceIdToManager[surfaceId] !== surfaceMountingManager) { + logSoftException( + TAG, + IllegalStateException( + "Called startSurface more than once for the SurfaceId [$surfaceId]")) + } + + mostRecentSurfaceMountingManager = surfaceIdToManager[surfaceId] + + if (rootView != null) { + surfaceMountingManager.attachRootView(rootView, reactContext) + } + + return surfaceMountingManager + } + + @AnyThread + fun attachRootView(surfaceId: Int, rootView: View?, themedReactContext: ThemedReactContext?) { + val surfaceMountingManager = getSurfaceManagerEnforced(surfaceId, "attachView") + + if (surfaceMountingManager.isStopped) { + logSoftException(TAG, IllegalStateException("Trying to attach a view to a stopped surface")) + return + } + + surfaceMountingManager.attachRootView(rootView, themedReactContext) + } + + @AnyThread + fun stopSurface(surfaceId: Int) { + val surfaceMountingManager = surfaceIdToManager[surfaceId] + if (surfaceMountingManager != null) { + // Maximum number of stopped surfaces to keep track of + while (stoppedSurfaceIds.size >= MAX_STOPPED_SURFACE_IDS_LENGTH) { + val staleStoppedId = stoppedSurfaceIds[0] + checkNotNull(staleStoppedId) + surfaceIdToManager.remove(staleStoppedId) + stoppedSurfaceIds.remove(staleStoppedId) + FLog.d(TAG, "Removing stale SurfaceMountingManager: [%d]", staleStoppedId) + } + stoppedSurfaceIds.add(surfaceId) + + surfaceMountingManager.stopSurface() + + if (mostRecentSurfaceMountingManager === surfaceMountingManager) { + mostRecentSurfaceMountingManager = null + } + if (lastQueriedSurfaceMountingManager === surfaceMountingManager) { + lastQueriedSurfaceMountingManager = null + } + } else { + logSoftException( + TAG, + IllegalStateException("Cannot call stopSurface on non-existent surface: [$surfaceId]")) + } + } + + fun getSurfaceManager(surfaceId: Int): SurfaceMountingManager? { + if (lastQueriedSurfaceMountingManager?.surfaceId == surfaceId) { + return lastQueriedSurfaceMountingManager + } + + if (mostRecentSurfaceMountingManager?.surfaceId == surfaceId) { + return mostRecentSurfaceMountingManager + } + + val surfaceMountingManager = surfaceIdToManager[surfaceId] + lastQueriedSurfaceMountingManager = surfaceMountingManager + return surfaceMountingManager + } + + fun getSurfaceManagerEnforced(surfaceId: Int, context: String): SurfaceMountingManager = + getSurfaceManager(surfaceId) + ?: throw RetryableMountingLayerException( + ("Unable to find SurfaceMountingManager for surfaceId: [$surfaceId]. Context: $context")) + + fun surfaceIsStopped(surfaceId: Int): Boolean { + if (stoppedSurfaceIds.contains(surfaceId)) { + return true + } + + val surfaceMountingManager = getSurfaceManager(surfaceId) + return surfaceMountingManager != null && surfaceMountingManager.isStopped + } + + fun isWaitingForViewAttach(surfaceId: Int): Boolean { + val mountingManager = getSurfaceManager(surfaceId) ?: return false + + if (mountingManager.isStopped) { + return false + } + + return !mountingManager.isRootViewAttached + } + + /** + * Get SurfaceMountingManager associated with a ReactTag. Unfortunately, this requires lookups + * over N maps, where N is the number of active or recently-stopped Surfaces. Each lookup will + * cost `log(M)` operations where M is the number of reactTags in the surface, so the total cost + * per lookup is `O(N * log(M))`. + * + * To mitigate this cost, we attempt to keep track of the "most recent" SurfaceMountingManager and + * do lookups in it first. For the vast majority of use-cases, except for events or operations + * sent to off-screen surfaces, or use-cases where multiple surfaces are visible and interactable, + * this will reduce the lookup time to `O(log(M))`. Someone smarter than me could probably figure + * out an amortized time. + * + * @param reactTag + * @return + */ + fun getSurfaceManagerForView(reactTag: Int): SurfaceMountingManager? { + if (mostRecentSurfaceMountingManager?.getViewExists(reactTag) == true) { + return mostRecentSurfaceMountingManager + } + + for ((_, smm) in surfaceIdToManager) { + if (smm !== mostRecentSurfaceMountingManager && smm.getViewExists(reactTag)) { + if (mostRecentSurfaceMountingManager == null) { + mostRecentSurfaceMountingManager = smm + } + return smm + } + } + return null + } + + @AnyThread + fun getSurfaceManagerForViewEnforced(reactTag: Int): SurfaceMountingManager = + getSurfaceManagerForView(reactTag) + ?: throw RetryableMountingLayerException( + "Unable to find SurfaceMountingManager for tag: [$reactTag]") + + fun getViewExists(reactTag: Int): Boolean = getSurfaceManagerForView(reactTag) != null + + @Deprecated( + "receiveCommand with Int is deprecated, you should use receiveCommand with commandId:String", + ReplaceWith("receiveCommand(Int,Int,String,ReadableArray)")) + fun receiveCommand(surfaceId: Int, reactTag: Int, commandId: Int, commandArgs: ReadableArray) { + assertOnUiThread() + @Suppress("DEPRECATION") + getSurfaceManagerEnforced(surfaceId, "receiveCommand:int") + .receiveCommand(reactTag, commandId, commandArgs) + } + + fun receiveCommand( + surfaceId: Int, + reactTag: Int, + commandId: String?, + commandArgs: ReadableArray + ) { + assertOnUiThread() + getSurfaceManagerEnforced(surfaceId, "receiveCommand:string") + .receiveCommand(reactTag, checkNotNull(commandId), commandArgs) + } + + /** + * Send an accessibility eventType to a Native View. eventType is any valid `AccessibilityEvent.X` + * value. + * + * Why accept {@ViewUtil.NO_SURFACE_ID}(-1) SurfaceId? Currently there are calls to + * UIManager.sendAccessibilityEvent which is a legacy API and accepts only reactTag. We will have + * to investigate and migrate away from those calls over time. + * + * @param surfaceId that identifies the surface or {@ViewUtil.NO_SURFACE_ID}(-1) to temporarily + * support backward compatibility. + * @param reactTag that identifies the react Tag of the view. + * @param eventType that identifies Android eventType. see [View.sendAccessibilityEvent] + */ + fun sendAccessibilityEvent(surfaceId: Int, reactTag: Int, eventType: Int) { + assertOnUiThread() + if (surfaceId == View.NO_ID) { + getSurfaceManagerForViewEnforced(reactTag).sendAccessibilityEvent(reactTag, eventType) + } else { + getSurfaceManagerEnforced(surfaceId, "sendAccessibilityEvent") + .sendAccessibilityEvent(reactTag, eventType) + } + } + + @UiThread + fun updateProps(reactTag: Int, props: ReadableMap?) { + assertOnUiThread() + if (props == null) { + return + } + + getSurfaceManagerForViewEnforced(reactTag).updateProps(reactTag, props) + } + + /** + * Clears the JS Responder specified by [SurfaceMountingManager.setJSResponder]. After this method + * is called, all the touch events are going to be handled by JS. + */ + @UiThread + fun clearJSResponder() { + // MountingManager and SurfaceMountingManagers all share the same JSResponderHandler. + // Must be called on MountingManager instead of SurfaceMountingManager, because we don't + // know what surfaceId it's being called for. + jsResponderHandler.clearJSResponder() + } + + @AnyThread + @ThreadConfined(ThreadConfined.ANY) + fun getEventEmitter(surfaceId: Int, reactTag: Int): EventEmitterWrapper? = + getSurfaceMountingManager(surfaceId, reactTag)?.getEventEmitter(reactTag) + + /** + * Measure a component, given localData, props, state, and measurement information. This needs to + * remain here for now - and not in SurfaceMountingManager - because sometimes measures are made + * outside of the context of a Surface; especially from C++ before StartSurface is called. + */ + @AnyThread + fun measure( + context: ReactContext?, + componentName: String?, + localData: ReadableMap?, + props: ReadableMap?, + state: ReadableMap?, + width: Float, + widthMode: YogaMeasureMode?, + height: Float, + heightMode: YogaMeasureMode?, + attachmentsPositions: FloatArray? + ): Long = + viewManagerRegistry + .get(checkNotNull(componentName)) + .measure( + context, + localData, + props, + state, + width, + widthMode, + height, + heightMode, + attachmentsPositions) + + /** + * This prefetch method is experimental, do not use it for production code. it will most likely + * change or be removed in the future. + * + * @param reactContext + * @param componentName + * @param surfaceId surface ID + * @param reactTag reactTag that should be set as ID of the view instance + * @param params prefetch request params defined in C++ + */ + @Suppress("FunctionName") + @AnyThread + @UnstableReactNativeAPI + fun experimental_prefetchResource( + reactContext: ReactContext?, + componentName: String?, + surfaceId: Int, + reactTag: Int, + params: MapBuffer? + ) { + viewManagerRegistry + .get(checkNotNull(componentName)) + .experimental_prefetchResource(reactContext, surfaceId, reactTag, params) + } + + fun enqueuePendingEvent( + surfaceId: Int, + reactTag: Int, + eventName: String?, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int + ) { + val smm = getSurfaceMountingManager(surfaceId, reactTag) + if (smm == null) { + FLog.d( + TAG, + "Cannot queue event without valid surface mounting manager for tag: %d, surfaceId: %d", + reactTag, + surfaceId) + return + } + smm.enqueuePendingEvent(reactTag, eventName, canCoalesceEvent, params, eventCategory) + } + + private fun getSurfaceMountingManager(surfaceId: Int, reactTag: Int): SurfaceMountingManager? = + if (surfaceId == ViewUtil.NO_SURFACE_ID) getSurfaceManagerForView(reactTag) + else getSurfaceManager(surfaceId) + + companion object { + val TAG: String = MountingManager::class.java.simpleName + private const val MAX_STOPPED_SURFACE_IDS_LENGTH = 15 + } +}