Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Events on the Fabric Interop Layer #37059

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.interop.InteropModuleRegistry;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.common.LifecycleState;
Expand Down Expand Up @@ -69,6 +70,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
private @Nullable JSExceptionHandler mJSExceptionHandler;
private @Nullable JSExceptionHandler mExceptionHandlerWrapper;
private @Nullable WeakReference<Activity> mCurrentActivity;

private @Nullable InteropModuleRegistry mInteropModuleRegistry;
private boolean mIsInitialized = false;

public ReactContext(Context base) {
Expand All @@ -93,6 +96,7 @@ public void initializeWithInstance(CatalystInstance catalystInstance) {

ReactQueueConfiguration queueConfig = catalystInstance.getReactQueueConfiguration();
initializeMessageQueueThreads(queueConfig);
initializeInteropModules();
}

/** Initialize message queue threads using a ReactQueueConfiguration. */
Expand Down Expand Up @@ -120,6 +124,14 @@ public synchronized void initializeMessageQueueThreads(ReactQueueConfiguration q
mIsInitialized = true;
}

protected void initializeInteropModules() {
mInteropModuleRegistry = new InteropModuleRegistry();
}

protected void initializeInteropModules(ReactContext reactContext) {
mInteropModuleRegistry = reactContext.mInteropModuleRegistry;
}

public void resetPerfStats() {
if (mNativeModulesMessageQueueThread != null) {
mNativeModulesMessageQueueThread.resetPerfStats();
Expand Down Expand Up @@ -163,6 +175,10 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
}
throw new IllegalStateException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE);
}
if (mInteropModuleRegistry != null
&& mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) {
return mInteropModuleRegistry.getInteropModule(jsInterface);
}
return mCatalystInstance.getJSModule(jsInterface);
}

Expand Down Expand Up @@ -543,4 +559,17 @@ public void registerSegment(int segmentId, String path, Callback callback) {
Assertions.assertNotNull(mCatalystInstance).registerSegment(segmentId, path);
Assertions.assertNotNull(callback).invoke();
}

/**
* Register a {@link JavaScriptModule} within the Interop Layer so that can be consumed whenever
* getJSModule is invoked.
*
* <p>This method is internal to React Native and should not be used externally.
*/
public <T extends JavaScriptModule> void internal_registerInteropModule(
Class<T> interopModuleInterface, Object interopModule) {
if (mInteropModuleRegistry != null) {
mInteropModuleRegistry.registerInteropModule(interopModuleInterface, interopModule);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.bridge.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.config.ReactFeatureFlags;
import java.util.HashMap;

/**
* A utility class that takes care of returning {@link JavaScriptModule} which are used for the
* Fabric Interop Layer. This allows us to override the returned classes once the user is invoking
* `ReactContext.getJsModule()`.
*
* <p>Currently we only support a `RCTEventEmitter` re-implementation, being `InteropEventEmitter`
* but this class can support other re-implementation in the future.
*/
public class InteropModuleRegistry {

@SuppressWarnings("rawtypes")
private final HashMap<Class, Object> supportedModules;

public InteropModuleRegistry() {
this.supportedModules = new HashMap<>();
}

public <T extends JavaScriptModule> boolean shouldReturnInteropModule(Class<T> requestedModule) {
return checkReactFeatureFlagsConditions() && supportedModules.containsKey(requestedModule);
}

@Nullable
public <T extends JavaScriptModule> T getInteropModule(Class<T> requestedModule) {
if (checkReactFeatureFlagsConditions()) {
//noinspection unchecked
return (T) supportedModules.get(requestedModule);
} else {
return null;
}
}

public <T extends JavaScriptModule> void registerInteropModule(
Class<T> interopModuleInterface, Object interopModule) {
if (checkReactFeatureFlagsConditions()) {
supportedModules.put(interopModuleInterface, interopModule);
}
}

private boolean checkReactFeatureFlagsConditions() {
return ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public class ReactFeatureFlags {
*/
public static volatile boolean enableFabricRenderer = false;

/**
* Should this application enable the Fabric Interop Layer for Android? If yes, the application
* will behave so that it can accept non-Fabric components and render them on Fabric. This toggle
* is controlling extra logic such as custom event dispatching that are needed for the Fabric
* Interop Layer to work correctly.
*/
public static volatile boolean unstable_useFabricInterop = false;

/**
* Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable
* the following flags: `useTurboModules` & `enableFabricRenderer`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object DefaultNewArchitectureEntryPoint {
) {
ReactFeatureFlags.useTurboModules = turboModulesEnabled
ReactFeatureFlags.enableFabricRenderer = fabricEnabled
ReactFeatureFlags.unstable_useFabricInterop = fabricEnabled

this.privateFabricEnabled = fabricEnabled
this.privateTurboModulesEnabled = turboModulesEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.facebook.react.fabric.events.EventBeatManager;
import com.facebook.react.fabric.events.EventEmitterWrapper;
import com.facebook.react.fabric.events.FabricEventEmitter;
import com.facebook.react.fabric.interop.InteropEventEmitter;
import com.facebook.react.fabric.mounting.MountItemDispatcher;
import com.facebook.react.fabric.mounting.MountingManager;
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
Expand All @@ -77,6 +78,7 @@
import com.facebook.react.uimanager.events.EventCategoryDef;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.EventDispatcherImpl;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
import java.util.HashMap;
Expand Down Expand Up @@ -383,6 +385,11 @@ public void initialize() {

ReactMarker.addFabricListener(mDevToolsReactPerfLogger);
}
if (ReactFeatureFlags.unstable_useFabricInterop) {
InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext);
mReactApplicationContext.internal_registerInteropModule(
RCTEventEmitter.class, interopEventEmitter);
}
}

// This is called on the JS thread (see CatalystInstanceImpl).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.events.Event;

/**
* An {@link Event} class used by the {@link InteropEventEmitter}. This class is just holding the
* event name and the data which is received by the `receiveEvent` method and will be passed over
* the the {@link com.facebook.react.uimanager.events.EventDispatcher}
*/
class InteropEvent extends Event<InteropEvent> {

private final String mName;
private final WritableMap mEventData;

InteropEvent(String name, @Nullable WritableMap eventData, int surfaceId, int viewTag) {
super(surfaceId, viewTag);
mName = name;
mEventData = eventData;
}

@Override
public String getEventName() {
return mName;
}

@Override
@VisibleForTesting
public WritableMap getEventData() {
return mEventData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;

/**
* A reimplementation of {@link RCTEventEmitter} which is using a {@link EventDispatcher} under the
* hood.
*
* <p>On Fabric, you're supposed to use {@link EventDispatcher} to dispatch events. However, we
* provide an interop layer for non-Fabric migrated components.
*
* <p>This instance will be returned if the user is invoking `context.getJsModule(RCTEventEmitter)
* and is providing support for the `receiveEvent` method, so that non-Fabric ViewManagers can
* continue to deliver events also when Fabric is turned on.
*/
public class InteropEventEmitter implements RCTEventEmitter {

private final ReactContext mReactContext;

private @Nullable EventDispatcher mEventDispatcherOverride;

public InteropEventEmitter(ReactContext reactContext) {
mReactContext = reactContext;
}

@Override
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap eventData) {
EventDispatcher dispatcher;
if (mEventDispatcherOverride != null) {
dispatcher = mEventDispatcherOverride;
} else {
dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, targetReactTag);
}
int surfaceId = UIManagerHelper.getSurfaceId(mReactContext);
if (dispatcher != null) {
dispatcher.dispatchEvent(new InteropEvent(eventName, eventData, surfaceId, targetReactTag));
}
}

@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
throw new UnsupportedOperationException(
"EventEmitter#receiveTouches is not supported by the Fabric Interop Layer");
}

@VisibleForTesting
void overrideEventDispatcher(EventDispatcher eventDispatcherOverride) {
mEventDispatcherOverride = eventDispatcherOverride;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public ThemedReactContext(
if (reactApplicationContext.hasCatalystInstance()) {
initializeWithInstance(reactApplicationContext.getCatalystInstance());
}
initializeInteropModules(reactApplicationContext);
mReactApplicationContext = reactApplicationContext;
mModuleName = moduleName;
mSurfaceId = surfaceId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.systrace.SystraceMessage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from
Expand Down Expand Up @@ -113,6 +117,13 @@

Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants();
if (viewManagerBubblingEvents != null) {

if (ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop) {
// For Fabric, events needs to be fired with a "top" prefix.
// For the sake of Fabric Interop, here we normalize events adding "top" in their
// name if the user hasn't provided it.
normalizeEventTypes(viewManagerBubblingEvents);
}
recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents);
recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents);
viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents);
Expand Down Expand Up @@ -145,6 +156,27 @@
return viewManagerConstants;
}

@VisibleForTesting
/* package */ static void normalizeEventTypes(Map events) {
if (events == null) {
return;
}
Set<String> keysToNormalize = new HashSet<>();
for (Object key : events.keySet()) {
if (key instanceof String) {
String keyString = (String) key;
if (!keyString.startsWith("top")) {
keysToNormalize.add(keyString);
}
}
}
for (String oldKey : keysToNormalize) {
Object value = events.get(oldKey);
String newKey = "top" + oldKey.substring(0, 1).toUpperCase() + oldKey.substring(1);
events.put(newKey, value);
}
}

/** Merges {@param source} map into {@param dest} map recursively */
private static void recursiveMerge(@Nullable Map dest, @Nullable Map source) {
if (dest == null || source == null || source.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.bridge.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;

public class FakeRCTEventEmitter implements RCTEventEmitter {

@Override
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap event) {}

@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {}
}
Loading