diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-format b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-format new file mode 100644 index 00000000000..bc20b078f0e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-format @@ -0,0 +1,91 @@ +--- +AccessModifierOffset: -1 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: true +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [ FOR_EACH_RANGE, FOR_EACH, ] +IncludeCategories: + - Regex: '^<.*\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +--- +Language: ObjC +ColumnLimit: 120 +BreakBeforeBraces: WebKit +... diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-tidy b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-tidy new file mode 100644 index 00000000000..376b86838ff --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/.clang-tidy @@ -0,0 +1,22 @@ +--- +# NOTE there must be no spaces before the '-', so put the comma last. +InheritParentConfig: true +Checks: ' +-, +-cert-err60-cpp, +-cppcoreguidelines-pro-bounds-pointer-arithmetic, +-cppcoreguidelines-special-member-functions, +-cppcoreguidelines-pro-type-const-cast, +-fuchsia-default-arguments-calls, +-fuchsia-multiple-inheritance, +-google-readability-casting, +-google-runtime-int, +-google-runtime-references, +-hicpp-special-member-functions, +-llvm-header-guard, +-misc-non-private-member-variables-in-classes, +-misc-unused-parameters, +-modernize-use-trailing-return-type, +-performance-unnecessary-value-param, +' +... diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessAtomicRef.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessAtomicRef.java new file mode 100644 index 00000000000..4ceb305c30f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessAtomicRef.java @@ -0,0 +1,130 @@ +/* + * 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.bridgeless; + +import static com.facebook.infer.annotation.Assertions.assertNotNull; + +import android.annotation.SuppressLint; +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; + +@Nullsafe(Nullsafe.Mode.LOCAL) +public class BridgelessAtomicRef { + + interface Provider { + T get(); + } + + @Nullable volatile T mValue; + @Nullable T mInitialValue; + + enum State { + Init, + Creating, + Success, + Failure + } + + volatile State state; + volatile String failureMessage; + + public BridgelessAtomicRef(@Nullable T initialValue) { + mValue = initialValue; + mInitialValue = initialValue; + state = State.Init; + failureMessage = ""; + } + + public BridgelessAtomicRef() { + this(null); + } + + @SuppressLint("CatchGeneralException") + public T getOrCreate(BridgelessAtomicRef.Provider provider) { + boolean shouldCreate = false; + synchronized (this) { + if (state == State.Success) { + return get(); + } + + if (state == State.Failure) { + throw new RuntimeException( + "BridgelessAtomicRef: Failed to create object. Reason: " + failureMessage); + } + + if (state != State.Creating) { + state = State.Creating; + shouldCreate = true; + } + } + + if (shouldCreate) { + try { + // Call provider with lock on `this` released to mitigate deadlock hazard + mValue = provider.get(); + + synchronized (this) { + state = State.Success; + notifyAll(); + return get(); + } + } catch (RuntimeException ex) { + synchronized (this) { + state = State.Failure; + String message = ex.getMessage(); + failureMessage = message != null ? message : "null"; + notifyAll(); + } + + throw new RuntimeException("BridgelessAtomicRef: Failed to create object.", ex); + } + } + + synchronized (this) { + boolean wasInterrupted = false; + while (state == State.Creating) { + try { + wait(); + } catch (InterruptedException ex) { + wasInterrupted = true; + } + } + + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + + if (state == State.Failure) { + throw new RuntimeException( + "BridgelessAtomicRef: Failed to create object. Reason: " + failureMessage); + } + + return get(); + } + } + + public synchronized T getAndReset() { + T value = get(); + reset(); + return value; + } + + public synchronized void reset() { + mValue = mInitialValue; + state = State.Init; + failureMessage = ""; + } + + public synchronized T get() { + return assertNotNull(mValue); + } + + public synchronized @Nullable T getNullable() { + return mValue; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessDevSupportManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessDevSupportManager.java new file mode 100644 index 00000000000..fe387acefaf --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessDevSupportManager.java @@ -0,0 +1,152 @@ +/* + * 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.bridgeless; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import bolts.Continuation; +import bolts.Task; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.JavaJSExecutor; +import com.facebook.react.bridge.JavaScriptExecutorFactory; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.devsupport.DevSupportManagerBase; +import com.facebook.react.devsupport.HMRClient; +import com.facebook.react.devsupport.ReactInstanceDevHelper; +import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import javax.annotation.Nullable; + +/** + * An implementation of {@link com.facebook.react.devsupport.interfaces.DevSupportManager} that + * extends the functionality in {@link DevSupportManagerBase} with some additional, more flexible + * APIs for asynchronously loading the JS bundle. + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public class BridgelessDevSupportManager extends DevSupportManagerBase { + + private final ReactHost mReactHost; + + public BridgelessDevSupportManager( + ReactHost host, Context context, @Nullable String packagerPathForJSBundleName) { + super( + context.getApplicationContext(), + createInstanceDevHelper(host), + packagerPathForJSBundleName, + true /* enableOnCreate */, + null /* redBoxHandler */, + null /* devBundleDownloadListener */, + 2 /* minNumShakes */, + null /* customPackagerCommandHandlers */, + null /* surfaceDelegateFactory */, + null /* devLoadingViewManager */); + mReactHost = host; + } + + @Override + protected String getUniqueTag() { + return "Bridgeless"; + } + + @Override + public void loadSplitBundleFromServer( + final String bundlePath, final DevSplitBundleCallback callback) { + fetchSplitBundleAndCreateBundleLoader( + bundlePath, + new CallbackWithBundleLoader() { + @Override + public void onSuccess(final JSBundleLoader bundleLoader) { + mReactHost + .loadBundle(bundleLoader) + .onSuccess( + new Continuation() { + @Override + public Void then(Task task) { + if (task.getResult().equals(Boolean.TRUE)) { + String bundleURL = + getDevServerHelper().getDevServerSplitBundleURL(bundlePath); + ReactContext reactContext = mReactHost.getCurrentReactContext(); + if (reactContext != null) { + reactContext.getJSModule(HMRClient.class).registerBundle(bundleURL); + } + callback.onSuccess(); + } + return null; + } + }); + } + + @Override + public void onError(String url, Throwable cause) { + callback.onError(url, cause); + } + }); + } + + @Override + public void handleReloadJS() { + hideRedboxDialog(); + mReactHost.reload("BridgelessDevSupportManager.handleReloadJS()"); + } + + private static ReactInstanceDevHelper createInstanceDevHelper(final ReactHost reactHost) { + return new ReactInstanceDevHelper() { + @Override + public void onReloadWithJSDebugger(JavaJSExecutor.Factory proxyExecutorFactory) { + // Not implemented + } + + @Override + public void onJSBundleLoadedFromServer() { + throw new IllegalStateException("Not implemented for bridgeless mode"); + } + + @Override + public void toggleElementInspector() { + ReactContext reactContext = reactHost.getCurrentReactContext(); + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("toggleElementInspector", null); + } + } + + @androidx.annotation.Nullable + @Override + public Activity getCurrentActivity() { + return reactHost.getCurrentActivity(); + } + + @Override + public JavaScriptExecutorFactory getJavaScriptExecutorFactory() { + throw new IllegalStateException("Not implemented for bridgeless mode"); + } + + @androidx.annotation.Nullable + @Override + public View createRootView(String appKey) { + Activity currentActivity = getCurrentActivity(); + if (currentActivity != null && !reactHost.isSurfaceWithModuleNameAttached(appKey)) { + ReactSurface reactSurface = ReactSurface.createWithView(currentActivity, appKey, null); + reactSurface.attach(reactHost); + reactSurface.start(); + + return reactSurface.getView(); + } + return null; + } + + @Override + public void destroyRootView(View rootView) { + // Not implemented + } + }; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactContext.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactContext.java new file mode 100644 index 00000000000..bfbc1f4ce44 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactContext.java @@ -0,0 +1,160 @@ +/* + * 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.bridgeless; + +import android.content.Context; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JSIModule; +import com.facebook.react.bridge.JSIModuleType; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactNoCrashBridgeNotAllowedSoftException; +import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.EventDispatcherProvider; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + +/** + * This class is used instead of {@link ReactApplicationContext} when React Native is operating in + * bridgeless mode. The purpose of this class is to override some methods on {@link + * com.facebook.react.bridge.ReactContext} that use the {@link + * com.facebook.react.bridge.CatalystInstance}, which doesn't exist in bridgeless mode. + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public class BridgelessReactContext extends ReactApplicationContext + implements EventDispatcherProvider { + + private final ReactHost mReactHost; + private final AtomicReference mSourceURL = new AtomicReference<>(); + private final String TAG = this.getClass().getSimpleName(); + + BridgelessReactContext(Context context, ReactHost host) { + super(context); + mReactHost = host; + } + + @Override + public boolean isBridgeless() { + return true; + } + + @Override + public EventDispatcher getEventDispatcher() { + return mReactHost.getEventDispatcher(); + } + + public void setSourceURL(String sourceURL) { + mSourceURL.set(sourceURL); + } + + @Override + public @Nullable String getSourceURL() { + return mSourceURL.get(); + } + + @Override + public @Nullable JSIModule getJSIModule(JSIModuleType moduleType) { + if (moduleType == JSIModuleType.UIManager) { + return mReactHost.getUIManager(); + } + throw new UnsupportedOperationException( + "getJSIModule is not implemented for bridgeless mode. Trying to get module: " + + moduleType.name()); + } + + @Override + public CatalystInstance getCatalystInstance() { + ReactSoftExceptionLogger.logSoftExceptionVerbose( + TAG, + new ReactNoCrashBridgeNotAllowedSoftException( + "getCatalystInstance() cannot be called when the bridge is disabled")); + throw new UnsupportedOperationException("There is no Catalyst instance in bridgeless mode."); + } + + @Override + public boolean hasActiveReactInstance() { + return mReactHost.isInstanceInitialized(); + } + + public DevSupportManager getDevSupportManager() { + return mReactHost.getDevSupportManager(); + } + + @Override + public void registerSegment(int segmentId, String path, Callback callback) { + mReactHost.registerSegment(segmentId, path, callback); + } + + private static class BridgelessJSModuleInvocationHandler implements InvocationHandler { + private final ReactHost mReactHost; + private final Class mJSModuleInterface; + + public BridgelessJSModuleInvocationHandler( + ReactHost reactHost, Class jsModuleInterface) { + mReactHost = reactHost; + mJSModuleInterface = jsModuleInterface; + } + + @Override + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + NativeArray jsArgs = args != null ? Arguments.fromJavaArgs(args) : new WritableNativeArray(); + mReactHost.callFunctionOnModule( + JavaScriptModuleRegistry.getJSModuleName(mJSModuleInterface), method.getName(), jsArgs); + return null; + } + } + + @Override + public T getJSModule(Class jsInterface) { + JavaScriptModule interfaceProxy = + (JavaScriptModule) + Proxy.newProxyInstance( + jsInterface.getClassLoader(), + new Class[] {jsInterface}, + new BridgelessJSModuleInvocationHandler(mReactHost, jsInterface)); + return (T) interfaceProxy; + } + + @Override + public boolean hasNativeModule(Class nativeModuleInterface) { + return mReactHost.hasNativeModule(nativeModuleInterface); + } + + @Override + public Collection getNativeModules() { + return mReactHost.getNativeModules(); + } + + @Override + public @Nullable T getNativeModule(Class nativeModuleInterface) { + return mReactHost.getNativeModule(nativeModuleInterface); + } + + @Override + public void handleException(Exception e) { + mReactHost.handleException(e); + } + + public DefaultHardwareBackBtnHandler getDefaultHardwareBackBtnHandler() { + return mReactHost.getDefaultBackButtonHandler(); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactStateTracker.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactStateTracker.java new file mode 100644 index 00000000000..adf7a11c5ed --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/BridgelessReactStateTracker.java @@ -0,0 +1,35 @@ +/* + * 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.bridgeless; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Nullsafe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Nullsafe(Nullsafe.Mode.LOCAL) +public class BridgelessReactStateTracker { + final List mStates = Collections.synchronizedList(new ArrayList()); + final boolean mShouldTrackStates; + + BridgelessReactStateTracker(boolean shouldTrackStates) { + mShouldTrackStates = shouldTrackStates; + } + + public void enterState(String state) { + FLog.w("BridgelessReact", state); + if (mShouldTrackStates) { + mStates.add(state); + } + } + + public void assertStateOrder(String... expectedStates) { + // TODO: Implement + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSEngineInstance.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSEngineInstance.java new file mode 100644 index 00000000000..d4adeef240d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSEngineInstance.java @@ -0,0 +1,26 @@ +/* + * 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.bridgeless; + +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.soloader.SoLoader; + +@Nullsafe(Nullsafe.Mode.LOCAL) +public abstract class JSEngineInstance { + static { + SoLoader.loadLibrary("rninstance"); + } + + @DoNotStrip private HybridData mHybridData; + + protected JSEngineInstance(HybridData hybridData) { + mHybridData = hybridData; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSTimerExecutor.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSTimerExecutor.java new file mode 100644 index 00000000000..aa29cc707e8 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/JSTimerExecutor.java @@ -0,0 +1,49 @@ +/* + * 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.bridgeless; + +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.jni.HybridData; +import com.facebook.jni.annotations.DoNotStrip; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.modules.core.JavaScriptTimerExecutor; +import com.facebook.soloader.SoLoader; +import com.facebook.soloader.annotation.SoLoaderLibrary; + +@Nullsafe(Nullsafe.Mode.LOCAL) +@SoLoaderLibrary("rninstance") +public class JSTimerExecutor implements JavaScriptTimerExecutor { + + static { + SoLoader.loadLibrary("rninstance"); + } + + @DoNotStrip private HybridData mHybridData; + + public JSTimerExecutor(HybridData hybridData) { + mHybridData = hybridData; + } + + @Override + public void callTimers(WritableArray timerIDs) { + callTimers((WritableNativeArray) timerIDs); + } + + private native void callTimers(WritableNativeArray timerIDs); + + @Override + public void callIdleCallbacks(double frameTime) { + // TODO T52558331 + } + + @Override + public void emitTimeDriftWarning(String warningMessage) { + // TODO T52558331 + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactHost.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactHost.java new file mode 100644 index 00000000000..94c22180135 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactHost.java @@ -0,0 +1,1450 @@ +/* + * 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.bridgeless; + +import static com.facebook.infer.annotation.Assertions.assertNotNull; +import static com.facebook.infer.annotation.Assertions.nullsafeFIXME; +import static com.facebook.infer.annotation.ThreadConfined.UI; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.Nullable; +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; +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.infer.annotation.ThreadSafe; +import com.facebook.react.MemoryPressureRouter; +import com.facebook.react.ReactInstanceEventListener; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.MemoryPressureListener; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.ReactMarkerConstants; +import com.facebook.react.bridge.ReactNoCrashBridgeNotAllowedSoftException; +import com.facebook.react.bridge.ReactNoCrashSoftException; +import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +import com.facebook.react.bridge.queue.ReactQueueConfiguration; +import com.facebook.react.bridgeless.exceptionmanager.ReactJsExceptionHandler; +import com.facebook.react.common.LifecycleState; +import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.devsupport.DisabledDevSupportManager; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.events.BlackHoleEventDispatcher; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A ReactHost is an object that manages a single {@link ReactInstance}. A ReactHost can be + * constructed without initializing the ReactInstance, and it will continue to exist after the + * instance is destroyed. This class ensures safe access to the ReactInstance and the JS runtime; + * methods that operate on the instance use Bolts Tasks to defer the operation until the instance + * has been initialized. They also return a Task so the caller can be notified of completion. + * + * @see Bolts Android + */ +@ThreadSafe +@Nullsafe(Nullsafe.Mode.LOCAL) +public class ReactHost { + + // TODO T61403233 Make this configurable by product code + private static final boolean DEV = ReactBuildConfig.DEBUG; + + private static final int BRIDGELESS_MARKER_INSTANCE_KEY = 1; + + public static final String TAG = "ReactHost"; + + private final Context mContext; + private final ReactInstanceDelegate mReactInstanceDelegate; + private final ReactJsExceptionHandler mReactJsExceptionHandler; + private final DevSupportManager mDevSupportManager; + private final Executor mBGExecutor; + private final Executor mUIExecutor; + private final QueueThreadExceptionHandler mQueueThreadExceptionHandler; + private final Set mAttachedSurfaces = + Collections.synchronizedSet(new HashSet()); + private final MemoryPressureRouter mMemoryPressureRouter; + private final MemoryPressureListener mMemoryPressureListener; + private final boolean mAllowPackagerServerAccess; + private final boolean mUseDevSupport; + private final Collection mReactInstanceEventListeners = + Collections.synchronizedList(new ArrayList()); + + private final BridgelessAtomicRef> mReactInstanceTaskRef = + new BridgelessAtomicRef<>( + Task.forResult( + nullsafeFIXME( + (ReactInstance) null, + "forResult parameter supports null, but is not annotated as @Nullable"))); + + private final BridgelessAtomicRef mBridgelessReactContextRef = + new BridgelessAtomicRef<>(null); + + private final AtomicReference mActivity = new AtomicReference<>(); + private @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler; + private final BridgelessReactStateTracker mBridgelessReactStateTracker = + new BridgelessReactStateTracker(DEV); + private final ReactLifecycleStateManager mReactLifecycleStateManager = + new ReactLifecycleStateManager(mBridgelessReactStateTracker); + + private static AtomicInteger mCounter = new AtomicInteger(0); + private final int mId = mCounter.getAndIncrement(); + + public ReactHost( + Context context, + ReactInstanceDelegate delegate, + boolean allowPackagerServerAccess, + ReactJsExceptionHandler reactJsExceptionHandler, + boolean useDevSupport) { + this( + context, + delegate, + Executors.newSingleThreadExecutor(), + Task.UI_THREAD_EXECUTOR, + reactJsExceptionHandler, + allowPackagerServerAccess, + useDevSupport); + } + + public ReactHost( + Context context, + ReactInstanceDelegate delegate, + Executor bgExecutor, + Executor uiExecutor, + ReactJsExceptionHandler reactJsExceptionHandler, + boolean allowPackagerServerAccess, + boolean useDevSupport) { + mContext = context; + mReactInstanceDelegate = delegate; + mBGExecutor = bgExecutor; + mUIExecutor = uiExecutor; + mReactJsExceptionHandler = reactJsExceptionHandler; + mQueueThreadExceptionHandler = ReactHost.this::handleException; + mMemoryPressureRouter = new MemoryPressureRouter(context); + mMemoryPressureListener = + level -> + callWithExistingReactInstance( + "handleMemoryPressure(" + level + ")", + reactInstance -> reactInstance.handleMemoryPressure(level)); + mAllowPackagerServerAccess = allowPackagerServerAccess; + if (DEV) { + mDevSupportManager = + new BridgelessDevSupportManager( + ReactHost.this, mContext, mReactInstanceDelegate.getJSMainModulePath()); + } else { + mDevSupportManager = new DisabledDevSupportManager(); + } + mUseDevSupport = useDevSupport; + } + + public LifecycleState getLifecycleState() { + return mReactLifecycleStateManager.getLifecycleState(); + } + + /** + * This function can be used to initialize the ReactInstance in a background thread before a + * surface needs to be rendered. It is not necessary to call this function; startSurface() will + * initialize the ReactInstance if it hasn't been preloaded. + * + * @return A Task that completes when the instance is initialized. The task will be faulted if any + * errors occur during initialization, and will be cancelled if ReactHost.destroy() is called + * before it completes. + */ + public Task preload() { + if (ReactFeatureFlags.enableBridgelessArchitectureNewCreateReloadDestroy) { + return new_preload(); + } + + return old_preload(); + } + + @ThreadConfined("ReactHost") + private @Nullable Task mPreloadTask = null; + + public Task old_preload() { + final String method = "old_preload()"; + return Task.call( + () -> { + if (mPreloadTask == null) { + log(method, "Schedule"); + mPreloadTask = + getOrCreateReactInstanceTask() + .continueWithTask( + task -> { + if (task.isFaulted()) { + destroy( + "old_preload() failure: " + task.getError().getMessage(), + task.getError()); + mReactInstanceDelegate.handleException(task.getError()); + } + + return task; + }, + mBGExecutor) + .makeVoid(); + } + return mPreloadTask; + }, + mBGExecutor) + .continueWithTask(Task::getResult); + } + + public Task new_preload() { + final String method = "new_preload()"; + return Task.call( + () -> { + if (mPreloadTask == null) { + log(method, "Schedule"); + mPreloadTask = + waitThen_new_getOrCreateReactInstanceTask() + .continueWithTask( + (task) -> { + if (task.isFaulted()) { + mReactInstanceDelegate.handleException(task.getError()); + // Wait for destroy to finish + return new_getOrCreateDestroyTask( + "new_preload() failure: " + task.getError().getMessage(), + task.getError()) + .continueWithTask(destroyTask -> Task.forError(task.getError())) + .makeVoid(); + } + return task.makeVoid(); + }, + mBGExecutor); + } + return mPreloadTask; + }, + mBGExecutor) + .continueWithTask(Task::getResult); + } + + /** Initialize and run a React Native surface in a background without mounting real views. */ + public Task prerenderSurface(final ReactSurface surface) { + final String method = "prerenderSurface(surfaceId = " + surface.getSurfaceID() + ")"; + log(method, "Schedule"); + + attachSurface(surface); + return callAfterGetOrCreateReactInstance( + method, + reactInstance -> { + log(method, "Execute"); + reactInstance.prerenderSurface(surface); + }); + } + + /** + * Start rendering a React Native surface on screen. + * + * @param surface The ReactSurface to render + * @return A Task that will complete when startSurface has been called. + */ + public Task startSurface(final ReactSurface surface) { + final String method = "startSurface(surfaceId = " + surface.getSurfaceID() + ")"; + log(method, "Schedule"); + + attachSurface(surface); + return callAfterGetOrCreateReactInstance( + method, + reactInstance -> { + log(method, "Execute"); + reactInstance.startSurface(surface); + }); + } + + /** + * Stop rendering a React Native surface. + * + * @param surface The surface to stop + * @return A Task that will complete when stopSurface has been called. + */ + public Task stopSurface(final ReactSurface surface) { + final String method = "stopSurface(surfaceId = " + surface.getSurfaceID() + ")"; + log(method, "Schedule"); + + detachSurface(surface); + return callWithExistingReactInstance( + method, + reactInstance -> { + log(method, "Execute"); + reactInstance.stopSurface(surface); + }); + } + + /** + * To be called when the host activity is resumed. + * + * @param activity The host activity + */ + @ThreadConfined(UI) + public void onHostResume( + final @Nullable Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + mDefaultHardwareBackBtnHandler = defaultBackButtonImpl; + onHostResume(activity); + } + + @ThreadConfined(UI) + public void onHostResume(final @Nullable Activity activity) { + final String method = "onHostResume(activity)"; + log(method); + + mActivity.set(activity); + ReactContext currentContext = getCurrentReactContext(); + + // TODO(T137233065): Enable DevSupportManager here + mReactLifecycleStateManager.moveToOnHostResume(currentContext, mActivity.get()); + } + + @ThreadConfined(UI) + public void onHostPause(final @Nullable Activity activity) { + final String method = "onHostPause(activity)"; + log(method); + + ReactContext currentContext = getCurrentReactContext(); + + Activity currentActivity = mActivity.get(); + if (currentActivity != null) { + String currentActivityClass = currentActivity.getClass().getSimpleName(); + String activityClass = activity == null ? "null" : activity.getClass().getSimpleName(); + Assertions.assertCondition( + activity == currentActivity, + "Pausing an activity that is not the current activity, this is incorrect! " + + "Current activity: " + + currentActivityClass + + " " + + "Paused activity: " + + activityClass); + } + + // TODO(T137233065): Disable DevSupportManager here + mDefaultHardwareBackBtnHandler = null; + mReactLifecycleStateManager.moveToOnHostPause(currentContext, currentActivity); + } + + /** To be called when the host activity is paused. */ + @ThreadConfined(UI) + public void onHostPause() { + final String method = "onHostPause()"; + log(method); + + ReactContext currentContext = getCurrentReactContext(); + + // TODO(T137233065): Disable DevSupportManager here + mDefaultHardwareBackBtnHandler = null; + mReactLifecycleStateManager.moveToOnHostPause(currentContext, mActivity.get()); + } + + /** To be called when the host activity is destroyed. */ + @ThreadConfined(UI) + public void onHostDestroy() { + final String method = "onHostDestroy()"; + log(method); + + // TODO(T137233065): Disable DevSupportManager here + moveToHostDestroy(getCurrentReactContext()); + } + + @ThreadConfined(UI) + public void onHostDestroy(@Nullable Activity activity) { + final String method = "onHostDestroy(activity)"; + log(method); + + Activity currentActivity = mActivity.get(); + + // TODO(T137233065): Disable DevSupportManager here + if (currentActivity == activity) { + moveToHostDestroy(getCurrentReactContext()); + } + } + + @ThreadConfined(UI) + private void moveToHostDestroy(@Nullable ReactContext currentContext) { + mReactLifecycleStateManager.moveToOnHostDestroy(currentContext); + mActivity.set(null); + } + + /** + * Returns current ReactContext which could be nullable if ReactInstance hasn't been created. + * + * @return The {@link BridgelessReactContext} associated with ReactInstance. + */ + public @Nullable BridgelessReactContext getCurrentReactContext() { + return mBridgelessReactContextRef.getNullable(); + } + + public DevSupportManager getDevSupportManager() { + return assertNotNull(mDevSupportManager); + } + + public @Nullable Activity getCurrentActivity() { + return mActivity.get(); + } + + /** + * Get the {@link EventDispatcher} from the {@link FabricUIManager}. This always returns an + * EventDispatcher, even if the instance isn't alive; in that case, it returns a {@link + * BlackHoleEventDispatcher} which no-ops. + * + * @return The real {@link EventDispatcher} if the instance is alive; otherwise, a {@link + * BlackHoleEventDispatcher}. + */ + public EventDispatcher getEventDispatcher() { + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance == null) { + return BlackHoleEventDispatcher.get(); + } + + return reactInstance.getEventDispatcher(); + } + + public @Nullable FabricUIManager getUIManager() { + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance == null) { + return null; + } + return reactInstance.getUIManager(); + } + + public boolean hasNativeModule(Class nativeModuleInterface) { + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance != null) { + return reactInstance.hasNativeModule(nativeModuleInterface); + } + return false; + } + + public Collection getNativeModules() { + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance != null) { + return reactInstance.getNativeModules(); + } + return new ArrayList<>(); + } + + public @Nullable T getNativeModule(Class nativeModuleInterface) { + if (nativeModuleInterface == UIManagerModule.class) { + ReactSoftExceptionLogger.logSoftExceptionVerbose( + TAG, + new ReactNoCrashBridgeNotAllowedSoftException( + "getNativeModule(UIManagerModule.class) cannot be called when the bridge is disabled")); + } + + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance != null) { + return reactInstance.getNativeModule(nativeModuleInterface); + } + return null; + } + + public DefaultHardwareBackBtnHandler getDefaultBackButtonHandler() { + return () -> { + UiThreadUtil.assertOnUiThread(); + if (mDefaultHardwareBackBtnHandler != null) { + mDefaultHardwareBackBtnHandler.invokeDefaultOnBackPressed(); + } + }; + } + + public MemoryPressureRouter getMemoryPressureRouter() { + return mMemoryPressureRouter; + } + + public boolean isInstanceInitialized() { + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + return reactInstance != null; + } + + @ThreadConfined(UI) + public boolean onBackPressed() { + UiThreadUtil.assertOnUiThread(); + final ReactInstance reactInstance = mReactInstanceTaskRef.get().getResult(); + if (reactInstance == null) { + return false; + } + + DeviceEventManagerModule deviceEventManagerModule = + reactInstance.getNativeModule(DeviceEventManagerModule.class); + if (deviceEventManagerModule == null) { + return false; + } + + deviceEventManagerModule.emitHardwareBackPressed(); + return true; + } + + public @Nullable ReactQueueConfiguration getReactQueueConfiguration() { + synchronized (mReactInstanceTaskRef) { + Task task = mReactInstanceTaskRef.get(); + if (!task.isFaulted() && !task.isCancelled() && task.getResult() != null) { + return task.getResult().getReactQueueConfiguration(); + } + } + return null; + } + + /** Add a listener to be notified of ReactInstance events. */ + public void addReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.add(listener); + } + + /** Remove a listener previously added with {@link #addReactInstanceEventListener}. */ + public void removeReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.remove(listener); + } + + /* package */ Task loadBundle(final JSBundleLoader bundleLoader) { + final String method = "loadBundle()"; + log(method, "Schedule"); + + return callWithExistingReactInstance( + method, + reactInstance -> { + log(method, "Execute"); + reactInstance.loadJSBundle(bundleLoader); + }); + } + + /* package */ Task registerSegment( + final int segmentId, final String path, final Callback callback) { + final String method = + "registerSegment(segmentId = \"" + segmentId + "\", path = \"" + path + "\")"; + log(method, "Schedule"); + + return callWithExistingReactInstance( + method, + reactInstance -> { + log(method, "Execute"); + reactInstance.registerSegment(segmentId, path); + assertNotNull(callback).invoke(); + }); + } + + /*package */ void handleException(Exception e) { + final String method = "handleException(message = \"" + e.getMessage() + "\")"; + log(method); + + destroy(method, e); + mReactInstanceDelegate.handleException(e); + } + + /** + * Call a function on a JS module that has been registered as callable. + * + * @param moduleName The name of the JS module + * @param methodName The function to call + * @param args Arguments to be passed to the function + * @return A Task that will complete when the function call has been enqueued on the JS thread. + */ + /* package */ Task callFunctionOnModule( + final String moduleName, final String methodName, final NativeArray args) { + final String method = "callFunctionOnModule(\"" + moduleName + "\", \"" + methodName + "\")"; + return callWithExistingReactInstance( + method, + reactInstance -> { + reactInstance.callFunctionOnModule(moduleName, methodName, args); + }); + } + + /* package */ void attachSurface(ReactSurface surface) { + final String method = "attachSurface(surfaceId = " + surface.getSurfaceID() + ")"; + log(method); + + synchronized (mAttachedSurfaces) { + mAttachedSurfaces.add(surface); + } + } + + /* package */ void detachSurface(ReactSurface surface) { + final String method = "detachSurface(surfaceId = " + surface.getSurfaceID() + ")"; + log(method); + + synchronized (mAttachedSurfaces) { + mAttachedSurfaces.remove(surface); + } + } + + boolean isSurfaceAttached(ReactSurface surface) { + synchronized (mAttachedSurfaces) { + return mAttachedSurfaces.contains(surface); + } + } + + boolean isSurfaceWithModuleNameAttached(String moduleName) { + synchronized (mAttachedSurfaces) { + for (ReactSurface surface : mAttachedSurfaces) { + if (surface.getModuleName().equals(moduleName)) { + return true; + } + } + return false; + } + } + + interface VeniceThenable { + void then(T t); + } + + private void raiseSoftException(String method, String message) { + raiseSoftException(method, message, null); + } + + private void raiseSoftException(String method, String message, @Nullable Throwable throwable) { + log(method, message); + if (ReactFeatureFlags.enableBridgelessArchitectureSoftExceptions) { + if (throwable != null) { + ReactSoftExceptionLogger.logSoftException( + TAG, new ReactNoCrashSoftException(method + ": " + message, throwable)); + return; + } + + ReactSoftExceptionLogger.logSoftException( + TAG, new ReactNoCrashSoftException(method + ": " + message)); + } + } + + private Task callWithExistingReactInstance( + final String callingMethod, final VeniceThenable continuation) { + final String method = "callWithExistingReactInstance(" + callingMethod + ")"; + + return mReactInstanceTaskRef + .get() + .onSuccess( + task -> { + final ReactInstance reactInstance = task.getResult(); + if (reactInstance == null) { + raiseSoftException(method, "Execute: ReactInstance null. Dropping work."); + return FALSE; + } + + continuation.then(reactInstance); + return TRUE; + }, + mBGExecutor); + } + + private Task callAfterGetOrCreateReactInstance( + final String callingMethod, final VeniceThenable runnable) { + final String method = "callAfterGetOrCreateReactInstance(" + callingMethod + ")"; + + return getOrCreateReactInstanceTask() + .onSuccess( + (Continuation) + task -> { + final ReactInstance reactInstance = task.getResult(); + if (reactInstance == null) { + raiseSoftException(method, "Execute: ReactInstance is null"); + return null; + } + + runnable.then(reactInstance); + return null; + }, + mBGExecutor) + .continueWith( + task -> { + if (task.isFaulted()) { + handleException(task.getError()); + } + return null; + }, + mBGExecutor); + } + + private BridgelessReactContext getOrCreateReactContext() { + final String method = "getOrCreateReactContext()"; + return mBridgelessReactContextRef.getOrCreate( + () -> { + log(method, "Creating BridgelessReactContext"); + return new BridgelessReactContext(mContext, ReactHost.this); + }); + } + + /** + * Entrypoint to create the ReactInstance. + * + *

If the ReactInstance is reloading, will return the reload task. If the ReactInstance is + * destroying, will wait until destroy is finished, before creating. + * + * @return + */ + private Task getOrCreateReactInstanceTask() { + final String method = "getOrCreateReactInstanceTask()"; + if (ReactFeatureFlags.enableBridgelessArchitectureNewCreateReloadDestroy) { + return Task.call(this::waitThen_new_getOrCreateReactInstanceTask, mBGExecutor) + .continueWithTask(Task::getResult); + } + + return old_getOrCreateReactInstanceTask(); + } + + @ThreadConfined("ReactHost") + private Task waitThen_new_getOrCreateReactInstanceTask() { + return waitThen_new_getOrCreateReactInstanceTaskWithRetries(0, 4); + } + + @ThreadConfined("ReactHost") + private Task waitThen_new_getOrCreateReactInstanceTaskWithRetries( + int tryNum, int maxTries) { + final String method = "waitThen_new_getOrCreateReactInstanceTaskWithRetries"; + if (mReloadTask != null) { + log(method, "React Native is reloading. Return reload task."); + return mReloadTask; + } + + if (mDestroyTask != null) { + boolean shouldTryAgain = tryNum < maxTries; + if (shouldTryAgain) { + log( + method, + "React Native is tearing down." + + "Wait for teardown to finish, before trying again (try count = " + + tryNum + + ")."); + return mDestroyTask.onSuccessTask( + (task) -> waitThen_new_getOrCreateReactInstanceTaskWithRetries(tryNum + 1, maxTries), + mBGExecutor); + } + + raiseSoftException( + method, + "React Native is tearing down. Not wait for teardown to finish: reached max retries."); + } + + return new_getOrCreateReactInstanceTask(); + } + + @ThreadConfined("ReactHost") + private Task new_getOrCreateReactInstanceTask() { + final String method = "new_getOrCreateReactInstanceTask()"; + log(method); + + return mReactInstanceTaskRef.getOrCreate( + () -> { + log(method, "Start"); + ReactMarker.logMarker( + ReactMarkerConstants.REACT_BRIDGELESS_LOADING_START, BRIDGELESS_MARKER_INSTANCE_KEY); + + return getJSBundleLoader() + .onSuccess( + task -> { + final JSBundleLoader bundleLoader = task.getResult(); + final BridgelessReactContext reactContext = getOrCreateReactContext(); + final DevSupportManager devSupportManager = getDevSupportManager(); + + log(method, "Creating ReactInstance"); + final ReactInstance instance = + new ReactInstance( + reactContext, + mReactInstanceDelegate, + devSupportManager, + mQueueThreadExceptionHandler, + mReactJsExceptionHandler, + mUseDevSupport); + + mMemoryPressureRouter.addMemoryPressureListener(mMemoryPressureListener); + + log(method, "Loading JS Bundle"); + instance.loadJSBundle(bundleLoader); + + log( + method, + "Calling DevSupportManagerBase.onNewReactContextCreated(reactContext)"); + devSupportManager.onNewReactContextCreated(reactContext); + + reactContext.runOnJSQueueThread( + () -> { + // Executing on the JS thread to ensurethat we're done + // loading the JS bundle. + // TODO T76081936 Move this if we switch to a sync RTE + ReactMarker.logMarker( + ReactMarkerConstants.REACT_BRIDGELESS_LOADING_END, + BRIDGELESS_MARKER_INSTANCE_KEY); + }); + + class Result { + final ReactInstance mInstance; + final ReactContext mContext; + + Result(ReactInstance instance, ReactContext context) { + mInstance = instance; + mContext = context; + } + } + + return new Result(instance, reactContext); + }, + mBGExecutor) + .onSuccess( + task -> { + ReactInstance reactInstance = task.getResult().mInstance; + ReactContext reactContext = task.getResult().mContext; + + /** + * Call ReactContext.onHostResume() only when already in the resumed state which + * aligns with the bridge https://fburl.com/diffusion/2qhxmudv. + */ + mReactLifecycleStateManager.resumeReactContextIfHostResumed( + reactContext, mActivity.get()); + + ReactInstanceEventListener[] listeners = + new ReactInstanceEventListener[mReactInstanceEventListeners.size()]; + final ReactInstanceEventListener[] finalListeners = + mReactInstanceEventListeners.toArray(listeners); + + log(method, "Executing ReactInstanceEventListeners"); + for (ReactInstanceEventListener listener : finalListeners) { + if (listener != null) { + listener.onReactContextInitialized(reactContext); + } + } + return reactInstance; + }, + mUIExecutor); + }); + } + + private Task old_getOrCreateReactInstanceTask() { + final String method = "old_getOrCreateReactInstanceTask()"; + log(method); + + return mReactInstanceTaskRef.getOrCreate( + () -> { + log(method, "Start"); + ReactMarker.logMarker( + ReactMarkerConstants.REACT_BRIDGELESS_LOADING_START, BRIDGELESS_MARKER_INSTANCE_KEY); + + final BridgelessReactContext reactContext = getOrCreateReactContext(); + final DevSupportManager devSupportManager = getDevSupportManager(); + + return getJSBundleLoader() + .onSuccess( + task -> { + final JSBundleLoader bundleLoader = task.getResult(); + log(method, "Creating ReactInstance"); + final ReactInstance instance = + new ReactInstance( + reactContext, + mReactInstanceDelegate, + devSupportManager, + mQueueThreadExceptionHandler, + mReactJsExceptionHandler, + mUseDevSupport); + + mMemoryPressureRouter.addMemoryPressureListener(mMemoryPressureListener); + + log(method, "Loading JS Bundle"); + instance.loadJSBundle(bundleLoader); + + log( + method, + "Calling DevSupportManagerBase.onNewReactContextCreated(reactContext)"); + devSupportManager.onNewReactContextCreated(reactContext); + reactContext.runOnJSQueueThread( + () -> { + // Executing on the JS thread to ensurethat we're done + // loading the JS bundle. + // TODO T76081936 Move this if we switchto a sync RTE + ReactMarker.logMarker( + ReactMarkerConstants.REACT_BRIDGELESS_LOADING_END, + BRIDGELESS_MARKER_INSTANCE_KEY); + }); + return instance; + }, + mBGExecutor) + .onSuccess( + task -> { + /** + * Call ReactContext.onHostResume() only when already in the resumed state which + * aligns with the bridge https://fburl.com/diffusion/2qhxmudv. + */ + mReactLifecycleStateManager.resumeReactContextIfHostResumed( + reactContext, mActivity.get()); + + ReactInstanceEventListener[] listeners = + new ReactInstanceEventListener[mReactInstanceEventListeners.size()]; + final ReactInstanceEventListener[] finalListeners = + mReactInstanceEventListeners.toArray(listeners); + + log(method, "Executing ReactInstanceEventListeners"); + for (ReactInstanceEventListener listener : finalListeners) { + if (listener != null) { + listener.onReactContextInitialized(reactContext); + } + } + + return task.getResult(); + }, + mUIExecutor); + }); + } + + private Task getJSBundleLoader() { + final String method = "getJSBundleLoader()"; + log(method); + + if (DEV && mAllowPackagerServerAccess) { + return isMetroRunning() + .onSuccessTask( + task -> { + boolean isMetroRunning = task.getResult(); + if (isMetroRunning) { + // Since metro is running, fetch the JS bundle from the server + return loadJSBundleFromMetro(); + } + return Task.forResult(mReactInstanceDelegate.getJSBundleLoader(mContext)); + }, + mBGExecutor); + } else { + if (DEV) { + FLog.d(TAG, "Packager server access is disabled in this environment"); + } + + /** + * In prod mode: fall back to the JS bundle loader from the delegate. + * + *

Note: Create the prod JSBundleLoader inside a Task.call. Why: If JSBundleLoader creation + * throws an exception, the task will fault, and we'll go through the ReactHost error + * reporting pipeline. + */ + return Task.call(() -> mReactInstanceDelegate.getJSBundleLoader(mContext)); + } + } + + private Task isMetroRunning() { + final String method = "isMetroRunning()"; + log(method); + + final TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + final DevSupportManager asyncDevSupportManager = getDevSupportManager(); + + asyncDevSupportManager.isPackagerRunning( + packagerIsRunning -> { + log(method, "Async result = " + packagerIsRunning); + taskCompletionSource.setResult(packagerIsRunning); + }); + + return taskCompletionSource.getTask(); + } + + /** + * TODO(T104078367): Ensure that if creating this JSBundleLoader fails, we route the errors + * through ReactHost's error reporting pipeline + */ + private Task loadJSBundleFromMetro() { + final String method = "loadJSBundleFromMetro()"; + log(method); + + final TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + final DevSupportManager asyncDevSupportManager = getDevSupportManager(); + final String sourceUrl = asyncDevSupportManager.getSourceUrl(); + + asyncDevSupportManager.reloadJSFromServer( + sourceUrl, + () -> { + log(method, "Creating BundleLoader"); + JSBundleLoader bundleLoader = + JSBundleLoader.createCachedBundleFromNetworkLoader( + sourceUrl, asyncDevSupportManager.getDownloadedJSBundleFile()); + taskCompletionSource.setResult(bundleLoader); + }); + + return taskCompletionSource.getTask(); + } + + private void log(String method, String message) { + mBridgelessReactStateTracker.enterState("ReactHost{" + mId + "}." + method + ": " + message); + } + + private void log(String method) { + mBridgelessReactStateTracker.enterState("ReactHost{" + mId + "}." + method); + } + + /** + * Entrypoint to reload the ReactInstance. If the ReactInstance is destroying, will wait until + * destroy is finished, before reloading. + * + * @return A task that completes when React Native reloads + */ + public Task reload(String reason) { + final String method = "reload()"; + if (ReactFeatureFlags.enableBridgelessArchitectureNewCreateReloadDestroy) { + return Task.call( + () -> { + if (mDestroyTask != null) { + log( + method, + "Destroying React Native. Waiting for destroy to finish, before reloading React Native."); + return mDestroyTask + .continueWithTask(task -> new_getOrCreateReloadTask(reason), mBGExecutor) + .makeVoid(); + } + + return new_getOrCreateReloadTask(reason).makeVoid(); + }, + mBGExecutor) + .continueWithTask(Task::getResult); + } + + return old_reload(reason); + } + + @ThreadConfined("ReactHost") + private @Nullable Task mReloadTask = null; + + /** + * The ReactInstance is loaded. Tear it down, and re-create it. + * + *

If the ReactInstance is in an "invalid state", make a "best effort" attempt to clean up + * React. "invalid state" means: ReactInstance task is faulted; ReactInstance is null; React + * instance task is cancelled; BridgelessReactContext is null. This can typically happen if the + * ReactInstance task work throws an exception. + */ + @ThreadConfined("ReactHost") + private Task new_getOrCreateReloadTask(String reason) { + final String method = "new_getOrCreateReloadTask()"; + log(method); + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason); + + if (mReloadTask == null) { + mReloadTask = + mReactInstanceTaskRef + .get() + .continueWithTask( + (task) -> { + log(method, "Starting on UI thread"); + + if (task.isFaulted()) { + raiseSoftException( + method, + "ReactInstance task faulted. Reload reason: " + reason, + task.getError()); + } + + if (task.isCancelled()) { + raiseSoftException( + method, "ReactInstance task cancelled. Reload reason: " + reason); + } + + final ReactInstance reactInstance = task.getResult(); + if (reactInstance == null) { + raiseSoftException(method, "ReactInstance is null. Reload reason: " + reason); + } + + final ReactContext reactContext = mBridgelessReactContextRef.getNullable(); + if (reactContext == null) { + raiseSoftException(method, "ReactContext is null. Reload reason: " + reason); + } + + if (reactContext != null + && mReactLifecycleStateManager.getLifecycleState() + == LifecycleState.RESUMED) { + log(method, "Calling ReactContext.onHostPause()"); + reactContext.onHostPause(); + } + + return task; + }, + mUIExecutor) + .continueWithTask( + task -> { + final ReactInstance reactInstance = task.getResult(); + + log(method, "Stopping all React Native surfaces"); + synchronized (mAttachedSurfaces) { + for (ReactSurface surface : mAttachedSurfaces) { + if (reactInstance != null) { + reactInstance.stopSurface(surface); + } + + surface.clear(); + } + } + + return task; + }, + mBGExecutor) + .continueWithTask( + task -> { + log(method, "Removing memory pressure listener"); + mMemoryPressureRouter.removeMemoryPressureListener(mMemoryPressureListener); + + final ReactContext reactContext = mBridgelessReactContextRef.getNullable(); + if (reactContext != null) { + log(method, "Destroying ReactContext"); + reactContext.destroy(); + } + + if (mUseDevSupport && reactContext != null) { + log( + method, + "Calling DevSupportManager.onReactInstanceDestroyed(reactContext)"); + mDevSupportManager.onReactInstanceDestroyed(reactContext); + } + + return task; + }, + mUIExecutor) + .continueWithTask( + task -> { + final ReactInstance reactInstance = task.getResult(); + + log(method, "Destroying ReactInstance"); + if (reactInstance != null) { + reactInstance.destroy(); + } + + log(method, "Resetting ReactContext ref"); + mBridgelessReactContextRef.reset(); + + log(method, "Resetting ReactInstance task ref"); + mReactInstanceTaskRef.reset(); + + log(method, "Resetting preload task ref"); + mPreloadTask = null; + + // Kickstart a new ReactInstance create + return new_getOrCreateReactInstanceTask(); + }, + mBGExecutor) + .onSuccess( + task -> { + final ReactInstance reactInstance = task.getResult(); + if (reactInstance != null) { + log(method, "Restarting previously running React Native Surfaces"); + + synchronized (mAttachedSurfaces) { + for (ReactSurface surface : mAttachedSurfaces) { + reactInstance.startSurface(surface); + } + } + } + return reactInstance; + }, + mBGExecutor) + .continueWithTask( + task -> { + if (task.isFaulted()) { + raiseSoftException( + method, + "Failed to re-created ReactInstance. Task faulted. Reload reason: " + + reason, + task.getError()); + } + + if (task.isCancelled()) { + raiseSoftException( + method, + "Failed to re-created ReactInstance. Task cancelled. Reload reason: " + + reason); + } + + log(method, "Resetting reload task ref"); + mReloadTask = null; + return task; + }, + mBGExecutor); + } + + return mReloadTask; + } + + /** + * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until + * reload is finished, before destroying. + * + * @return A task that completes when React Native gets destroyed. + */ + public Task destroy(String reason) { + return destroy(reason, null); + } + + public Task destroy(String reason, @Nullable Exception ex) { + final String method = "destroy()"; + if (ReactFeatureFlags.enableBridgelessArchitectureNewCreateReloadDestroy) { + return Task.call( + () -> { + if (mReloadTask != null) { + log( + method, + "Reloading React Native. Waiting for reload to finish before destroying React Native."); + return mReloadTask.continueWithTask( + task -> new_getOrCreateDestroyTask(reason, ex), mBGExecutor); + } + return new_getOrCreateDestroyTask(reason, ex); + }, + mBGExecutor) + .continueWithTask(Task::getResult); + } + + old_destroy(reason, ex); + return Task.forResult(nullsafeFIXME(null, "Empty Destroy Task")); + } + + @ThreadConfined("ReactHost") + private @Nullable Task mDestroyTask = null; + + /** + * The ReactInstance is loaded. Tear it down. + * + *

If the ReactInstance is in an "invalid state", make a "best effort" attempt to clean up + * React. "invalid state" means: ReactInstance task is faulted; ReactInstance is null; React + * instance task is cancelled; BridgelessReactContext is null. This can typically happen if the * + * ReactInstance task work throws an exception. + */ + @ThreadConfined("ReactHost") + private Task new_getOrCreateDestroyTask(final String reason, @Nullable Exception ex) { + final String method = "new_getOrCreateDestroyTask()"; + log(method); + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason, ex); + + if (mDestroyTask == null) { + mDestroyTask = + mReactInstanceTaskRef + .get() + .continueWithTask( + task -> { + log(method, "Destroying ReactInstance on UI Thread"); + + if (task.isFaulted()) { + raiseSoftException( + method, + "ReactInstance task faulted. Destroy reason: " + reason, + task.getError()); + } + + if (task.isCancelled()) { + raiseSoftException( + method, "ReactInstance task cancelled. Destroy reason: " + reason); + } + + final ReactInstance reactInstance = task.getResult(); + if (reactInstance == null) { + raiseSoftException( + method, "ReactInstance is null. Destroy reason: " + reason); + } + + // Step 1: Destroy DevSupportManager + if (mUseDevSupport) { + log(method, "DevSupportManager cleanup"); + // TODO(T137233065): Disable DevSupportManager here + mDevSupportManager.stopInspector(); + } + + final ReactContext reactContext = mBridgelessReactContextRef.getNullable(); + + if (reactContext == null) { + raiseSoftException(method, "ReactContext is null. Destroy reason: " + reason); + } + + // Step 2: Move React Native to onHostDestroy() + log(method, "Move ReactHost to onHostDestroy()"); + mReactLifecycleStateManager.moveToOnHostDestroy(reactContext); + + // Step 3: De-register the memory pressure listener + log(method, "Destroying MemoryPressureRouter"); + mMemoryPressureRouter.destroy(mContext); + + if (reactContext != null) { + log(method, "Destroying ReactContext"); + reactContext.destroy(); + } + + // Reset current activity + mActivity.set(null); + + // Clear ResourceIdleDrawableIdMap + ResourceDrawableIdHelper.getInstance().clear(); + + return task; + }, + mUIExecutor) + .continueWith( + task -> { + final ReactInstance reactInstance = task.getResult(); + if (reactInstance != null) { + log(method, "Destroying ReactInstance"); + reactInstance.destroy(); + } + + log(method, "Resetting ReactContext ref "); + mBridgelessReactContextRef.reset(); + + log(method, "Resetting ReactInstance task ref"); + mReactInstanceTaskRef.reset(); + + log(method, "Resetting Preload task ref"); + mPreloadTask = null; + + log(method, "Resetting destroy task ref"); + mDestroyTask = null; + return null; + }, + mBGExecutor); + } + + return mDestroyTask; + } + + /** Destroy and recreate the ReactInstance and context. */ + private Task old_reload(String reason) { + final String method = "old_reload()"; + log(method); + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason); + + synchronized (mReactInstanceTaskRef) { + mMemoryPressureRouter.removeMemoryPressureListener(mMemoryPressureListener); + old_destroyReactInstanceAndContext(method, reason); + + return callAfterGetOrCreateReactInstance( + method, + reactInstance -> { + // Restart any attached surfaces + log(method, "Restarting Surfaces"); + synchronized (mAttachedSurfaces) { + for (ReactSurface surface : mAttachedSurfaces) { + reactInstance.startSurface(surface); + } + } + }); + } + } + + /** Destroy the specified instance and context. */ + private void old_destroy(String reason, @Nullable Exception ex) { + final String method = "old_destroy()"; + log(method); + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason, ex); + + synchronized (mReactInstanceTaskRef) { + // Retain a reference to current ReactContext before de-referenced by mReactContextRef + final ReactContext reactContext = getCurrentReactContext(); + + if (reactContext != null) { + mMemoryPressureRouter.destroy(reactContext); + } + + old_destroyReactInstanceAndContext(method, reason); + + // Remove all attached surfaces + log(method, "Clearing attached surfaces"); + synchronized (mAttachedSurfaces) { + mAttachedSurfaces.clear(); + } + + Task.call( + (Callable) + () -> { + moveToHostDestroy(reactContext); + return null; + }, + mUIExecutor); + } + } + + private void old_destroyReactInstanceAndContext(final String callingMethod, final String reason) { + final String method = "old_destroyReactInstanceAndContext(" + callingMethod + ")"; + log(method); + + synchronized (mReactInstanceTaskRef) { + Task task = mReactInstanceTaskRef.getAndReset(); + if (!task.isFaulted() && !task.isCancelled()) { + final ReactInstance instance = task.getResult(); + + // Noop on redundant calls to destroyReactInstance() + if (instance == null) { + log(method, "ReactInstance nil"); + return; + } + + /* + * The surfaces should be stopped before the instance destroy. + * Calling stop directly on instance ensures we keep the list of attached surfaces for restart. + */ + log(method, "Stopping surfaces"); + synchronized (mAttachedSurfaces) { + for (ReactSurface surface : mAttachedSurfaces) { + instance.stopSurface(surface); + surface.clear(); + } + } + + ReactContext reactContext = getCurrentReactContext(); + + // Reset the ReactContext inside the DevSupportManager + if (reactContext != null) { + log(method, "DevSupportManager.onReactInstanceDestroyed()"); + getDevSupportManager().onReactInstanceDestroyed(reactContext); + log(method, "Destroy ReactContext"); + mBridgelessReactContextRef.reset(); + } + + mBGExecutor.execute( + () -> { + // instance.destroy() is time consuming and is confined to ReactHost thread. + log(method, "Destroy ReactInstance"); + instance.destroy(); + + // Re-enable preloads + log(method, "Resetting Preload task ref"); + mPreloadTask = null; + }); + } else { + raiseSoftException( + method, + ("Not cleaning up ReactInstance: task.isFaulted() = " + + task.isFaulted() + + ", task.isCancelled() = " + + task.isCancelled()) + + ". Reason: " + + reason); + + mBGExecutor.execute( + () -> { + log(method, "Resetting Preload task ref"); + mPreloadTask = null; + }); + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstance.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstance.java new file mode 100644 index 00000000000..fe093deda50 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstance.java @@ -0,0 +1,493 @@ +/* + * 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.bridgeless; + +import android.content.res.AssetManager; +import android.view.View; +import com.facebook.common.logging.FLog; +import com.facebook.fbreact.fabric.components.CatalystRegistry; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.infer.annotation.ThreadSafe; +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.ReactPackage; +import com.facebook.react.ViewManagerOnDemandReactPackage; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.JSBundleLoaderDelegate; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.bridge.RuntimeExecutor; +import com.facebook.react.bridge.RuntimeScheduler; +import com.facebook.react.bridge.queue.MessageQueueThread; +import com.facebook.react.bridge.queue.MessageQueueThreadSpec; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +import com.facebook.react.bridge.queue.ReactQueueConfiguration; +import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl; +import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.react.bridgeless.exceptionmanager.ReactJsExceptionHandler; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import com.facebook.react.fabric.Binding; +import com.facebook.react.fabric.BindingImpl; +import com.facebook.react.fabric.ComponentFactory; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.react.fabric.ReactNativeConfig; +import com.facebook.react.fabric.events.EventBeatManager; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.modules.core.JavaTimerManager; +import com.facebook.react.modules.core.ReactChoreographer; +import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; +import com.facebook.react.turbomodule.core.TurboModuleManager; +import com.facebook.react.turbomodule.core.TurboModuleManagerDelegate; +import com.facebook.react.uimanager.ComponentNameResolver; +import com.facebook.react.uimanager.ComponentNameResolverManager; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.uimanager.ViewManagerResolver; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.soloader.SoLoader; +import com.facebook.soloader.annotation.SoLoaderLibrary; +import com.facebook.systrace.Systrace; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * An experimental replacement for {@link com.facebook.react.ReactInstanceManager} responsible for + * creating and managing a React Native instance + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +@ThreadSafe +@SoLoaderLibrary({"rninstance"}) +public final class ReactInstance { + + private static final String TAG = ReactInstance.class.getSimpleName(); + + @DoNotStrip private HybridData mHybridData; + + private final ReactInstanceDelegate mDelegate; + private final BridgelessReactContext mBridgelessReactContext; + + private final ReactQueueConfiguration mQueueConfiguration; + private final TurboModuleManager mTurboModuleManager; + private final FabricUIManager mFabricUIManager; + private final JavaTimerManager mJavaTimerManager; + + @DoNotStrip @Nullable private ComponentNameResolverManager mComponentNameResolverManager; + + private static volatile boolean sIsLibraryLoaded; + + /* package */ ReactInstance( + BridgelessReactContext bridgelessReactContext, + ReactInstanceDelegate delegate, + DevSupportManager devSupportManager, + QueueThreadExceptionHandler exceptionHandler, + ReactJsExceptionHandler reactExceptionManager, + boolean useDevSupport) { + mBridgelessReactContext = bridgelessReactContext; + mDelegate = delegate; + loadLibraryIfNeeded(); + + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.initialize"); + + /** + * Prepare the ReactInstance by installing JSI bindings, initializing Fabric + TurboModules, and + * loading the JS bundle. + */ + MessageQueueThreadSpec nativeModulesSpec = + MessageQueueThreadSpec.newBackgroundThreadSpec("v_native"); + ReactQueueConfigurationSpec spec = + ReactQueueConfigurationSpec.builder() + .setJSQueueThreadSpec(MessageQueueThreadSpec.newBackgroundThreadSpec("v_js")) + .setNativeModulesQueueThreadSpec(nativeModulesSpec) + .build(); + mQueueConfiguration = ReactQueueConfigurationImpl.create(spec, exceptionHandler); + FLog.d(TAG, "Calling initializeMessageQueueThreads()"); + mBridgelessReactContext.initializeMessageQueueThreads(mQueueConfiguration); + MessageQueueThread jsMessageQueueThread = mQueueConfiguration.getJSQueueThread(); + MessageQueueThread nativeModulesMessageQueueThread = + mQueueConfiguration.getNativeModulesQueueThread(); + + ReactChoreographer.initialize(); + if (useDevSupport) { + devSupportManager.startInspector(); + } + JSTimerExecutor jsTimerExecutor = createJSTimerExecutor(); + mJavaTimerManager = + new JavaTimerManager( + mBridgelessReactContext, + jsTimerExecutor, + ReactChoreographer.getInstance(), + devSupportManager); + + mBridgelessReactContext.addLifecycleEventListener( + new LifecycleEventListener() { + @Override + public void onHostResume() { + mJavaTimerManager.onHostResume(); + } + + @Override + public void onHostPause() { + mJavaTimerManager.onHostPause(); + } + + @Override + public void onHostDestroy() { + mJavaTimerManager.onHostDestroy(); + } + }); + + JSEngineInstance jsEngineInstance = mDelegate.getJSEngineInstance(mBridgelessReactContext); + // Notify JS if profiling is enabled + boolean isProfiling = + Systrace.isTracing(Systrace.TRACE_TAG_REACT_APPS | Systrace.TRACE_TAG_REACT_JS_VM_CALLS); + mHybridData = + initHybrid( + jsEngineInstance, + jsMessageQueueThread, + nativeModulesMessageQueueThread, + mJavaTimerManager, + jsTimerExecutor, + reactExceptionManager, + isProfiling); + + RuntimeExecutor unbufferedRuntimeExecutor = getUnbufferedRuntimeExecutor(); + + // Initialize function for JS's UIManager.hasViewManagerConfig() + mComponentNameResolverManager = + new ComponentNameResolverManager( + // Use unbuffered RuntimeExecutor to install binding + unbufferedRuntimeExecutor, + new ComponentNameResolver() { + @Override + public String[] getComponentNames() { + Collection viewManagerNames = getViewManagerNames(); + if (viewManagerNames == null) { + FLog.e(TAG, "No ViewManager names found"); + return new String[0]; + } + return viewManagerNames.toArray(new String[0]); + } + }); + + // Set up TurboModules + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.initialize#initTurboModules"); + TurboModuleManagerDelegate turboModuleManagerDelegate = + mDelegate.getTurboModuleManagerDelegate(mBridgelessReactContext); + mTurboModuleManager = + new TurboModuleManager( + // Use unbuffered RuntimeExecutor to install binding + unbufferedRuntimeExecutor, + turboModuleManagerDelegate, + getJSCallInvokerHolder(), + getNativeCallInvokerHolder()); + + // Eagerly initialize TurboModules + for (String moduleName : mTurboModuleManager.getEagerInitModuleNames()) { + mTurboModuleManager.getNativeModule(moduleName); + } + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + + // Set up Fabric + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.initialize#initFabric"); + + ViewManagerRegistry viewManagerRegistry = + new ViewManagerRegistry( + new ViewManagerResolver() { + @Override + public @Nullable ViewManager getViewManager(String viewManagerName) { + return createViewManager(viewManagerName); + } + + @Override + public Collection getViewManagerNames() { + return ReactInstance.this.getViewManagerNames(); + } + }); + + EventBeatManager eventBeatManager = new EventBeatManager(mBridgelessReactContext); + mFabricUIManager = + new FabricUIManager(mBridgelessReactContext, viewManagerRegistry, eventBeatManager); + + ReactNativeConfig config = mDelegate.getReactNativeConfig(mTurboModuleManager); + ComponentFactory componentFactory = new ComponentFactory(); + // Using Catalyst for the defaults for now. This should be passed in from the Application. + CatalystRegistry.register(componentFactory); + + // Misc initialization that needs to be done before Fabric init + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(mBridgelessReactContext); + + Binding binding = new BindingImpl(); + binding.register( + getBufferedRuntimeExecutor(), + getRuntimeScheduler(), + mFabricUIManager, + eventBeatManager, + componentFactory, + config); + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + + // Initialize the FabricUIManager + mFabricUIManager.initialize(); + } + + private static synchronized void loadLibraryIfNeeded() { + if (!sIsLibraryLoaded) { + SoLoader.loadLibrary("rninstance"); + sIsLibraryLoaded = true; + } + } + + public ReactQueueConfiguration getReactQueueConfiguration() { + return mQueueConfiguration; + } + + public void loadJSBundle(JSBundleLoader bundleLoader) { + // Load the JS bundle + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.loadJSBundle"); + bundleLoader.loadScript( + new JSBundleLoaderDelegate() { + @Override + public void loadScriptFromFile( + String fileName, String sourceURL, boolean loadSynchronously) { + mBridgelessReactContext.setSourceURL(sourceURL); + loadJSBundleFromFile(fileName, sourceURL); + } + + @Override + public void loadSplitBundleFromFile(String fileName, String sourceURL) { + loadJSBundleFromFile(fileName, sourceURL); + } + + @Override + public void loadScriptFromAssets( + AssetManager assetManager, String assetURL, boolean loadSynchronously) { + mBridgelessReactContext.setSourceURL(assetURL); + loadJSBundleFromAssets(assetManager, assetURL); + } + + @Override + public void setSourceURLs(String deviceURL, String remoteURL) { + mBridgelessReactContext.setSourceURL(deviceURL); + } + }); + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + public boolean hasNativeModule(Class nativeModuleInterface) { + ReactModule annotation = nativeModuleInterface.getAnnotation(ReactModule.class); + if (annotation != null) { + return mTurboModuleManager.hasNativeModule(annotation.name()); + } + return false; + } + + public Collection getNativeModules() { + Collection nativeModules = new ArrayList<>(); + for (NativeModule module : mTurboModuleManager.getNativeModules()) { + nativeModules.add(module); + } + return nativeModules; + } + + public @Nullable T getNativeModule(Class nativeModuleInterface) { + ReactModule annotation = nativeModuleInterface.getAnnotation(ReactModule.class); + if (annotation != null) { + return (T) getNativeModule(annotation.name()); + } + return null; + } + + public @Nullable NativeModule getNativeModule(String nativeModuleName) { + synchronized (mTurboModuleManager) { + return mTurboModuleManager.getNativeModule(nativeModuleName); + } + } + + /* package */ void prerenderSurface(ReactSurface surface) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.prerenderSurface"); + FLog.d(TAG, "call prerenderSurface with surface: " + surface.getModuleName()); + mFabricUIManager.startSurface(surface.getSurfaceHandler(), surface.getContext(), null); + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + /** + * Renders a React Native surface. + * + * @param surface The {@link ReactSurface} to render. + */ + @ThreadConfined("ReactHost") + /* package */ void startSurface(ReactSurface surface) { + FLog.d(TAG, "startSurface() is called with surface: " + surface.getSurfaceID()); + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.startSurface"); + + View view = surface.getView(); + if (view == null) { + throw new IllegalStateException( + "Starting surface without a view is not supported, use prerenderSurface instead."); + } + + /** + * This is a temporary mitigation for 646912b2590a6d5e760316cc064d1e27, + * + *

TODO T83828172 investigate why surface.getView() has id NOT equal to View.NO_ID + */ + if (view.getId() != View.NO_ID) { + ReactSoftExceptionLogger.logSoftException( + TAG, + new IllegalViewOperationException( + "surfaceView's is NOT equal to View.NO_ID before calling startSurface.")); + view.setId(View.NO_ID); + } + if (surface.isRunning()) { + // surface was initialized beforehand, only attaching view + mFabricUIManager.attachRootView(surface.getSurfaceHandler(), view); + } else { + mFabricUIManager.startSurface(surface.getSurfaceHandler(), surface.getContext(), view); + } + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + @ThreadConfined("ReactHost") + /* package */ void stopSurface(ReactSurface surface) { + FLog.d(TAG, "stopSurface() is called with surface: " + surface.getSurfaceID()); + mFabricUIManager.stopSurface(surface.getSurfaceHandler()); + } + + /* --- Lifecycle methods --- */ + @ThreadConfined("ReactHost") + /* package */ void destroy() { + FLog.d(TAG, "ReactInstance.destroy() is called."); + mQueueConfiguration.destroy(); + mTurboModuleManager.onCatalystInstanceDestroy(); + mFabricUIManager.onCatalystInstanceDestroy(); + mHybridData.resetNative(); + mComponentNameResolverManager = null; + } + + /* --- Native methods --- */ + + @DoNotStrip + private native HybridData initHybrid( + JSEngineInstance jsEngineInstance, + MessageQueueThread jsMessageQueueThread, + MessageQueueThread nativeModulesMessageQueueThread, + JavaTimerManager timerManager, + JSTimerExecutor jsTimerExecutor, + ReactJsExceptionHandler jReactExceptionsManager, + boolean isProfiling); + + @DoNotStrip + private static native JSTimerExecutor createJSTimerExecutor(); + + @DoNotStrip + private native void installGlobals(boolean isProfiling); + + private native void loadJSBundleFromFile(String fileName, String sourceURL); + + private native void loadJSBundleFromAssets(AssetManager assetManager, String assetURL); + + private native CallInvokerHolderImpl getJSCallInvokerHolder(); + + private native CallInvokerHolderImpl getNativeCallInvokerHolder(); + + private native RuntimeExecutor getUnbufferedRuntimeExecutor(); + + private native RuntimeExecutor getBufferedRuntimeExecutor(); + + private native RuntimeScheduler getRuntimeScheduler(); + + /* package */ native void callFunctionOnModule( + String moduleName, String methodName, NativeArray args); + + private native void registerSegmentNative(int segmentId, String segmentPath); + + private native void handleMemoryPressureJs(int pressureLevel); + + public void handleMemoryPressure(int level) { + try { + handleMemoryPressureJs(level); + } catch (NullPointerException e) { + ReactSoftExceptionLogger.logSoftException( + TAG, + new IllegalViewOperationException( + "Native method handleMemoryPressureJs is called earlier than librninstance.so got ready.")); + } + } + + /** + * @return The {@link EventDispatcher} used by {@link FabricUIManager} to emit UI events to JS. + */ + /* protected */ EventDispatcher getEventDispatcher() { + return mFabricUIManager.getEventDispatcher(); + } + + /** @return The {@link FabricUIManager} if it's been initialized. */ + /* protected */ FabricUIManager getUIManager() { + return mFabricUIManager; + } + + public void registerSegment(int segmentId, String path) { + registerSegmentNative(segmentId, path); + } + + private @Nullable ViewManager createViewManager(String viewManagerName) { + if (mDelegate != null) { + List packages = mDelegate.getReactPackages(); + if (packages != null) { + synchronized (packages) { + for (ReactPackage reactPackage : packages) { + if (reactPackage instanceof ViewManagerOnDemandReactPackage) { + ViewManager viewManager = + ((ViewManagerOnDemandReactPackage) reactPackage) + .createViewManager(mBridgelessReactContext, viewManagerName); + if (viewManager != null) { + return viewManager; + } + } + } + } + } + } + + return null; + } + + private Collection getViewManagerNames() { + Set uniqueNames = new HashSet<>(); + if (mDelegate != null) { + List packages = mDelegate.getReactPackages(); + if (packages != null) { + synchronized (packages) { + for (ReactPackage reactPackage : packages) { + if (reactPackage instanceof ViewManagerOnDemandReactPackage) { + Collection names = + ((ViewManagerOnDemandReactPackage) reactPackage) + .getViewManagerNames(mBridgelessReactContext); + if (names != null) { + uniqueNames.addAll(names); + } + } + } + } + } + } + return uniqueNames; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstanceDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstanceDelegate.java new file mode 100644 index 00000000000..872ba9629c8 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactInstanceDelegate.java @@ -0,0 +1,38 @@ +/* + * 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.bridgeless; + +import android.content.Context; +import com.facebook.infer.annotation.ThreadSafe; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.fabric.ReactNativeConfig; +import com.facebook.react.turbomodule.core.TurboModuleManager; +import com.facebook.react.turbomodule.core.TurboModuleManagerDelegate; +import com.facebook.react.uimanager.ViewManager; +import java.util.List; + +@ThreadSafe +public interface ReactInstanceDelegate { + String getJSMainModulePath(); + + JSBundleLoader getJSBundleLoader(Context context); + + TurboModuleManagerDelegate getTurboModuleManagerDelegate(ReactApplicationContext context); + + List getViewManagers(ReactApplicationContext context); + + JSEngineInstance getJSEngineInstance(ReactApplicationContext context); + + void handleException(Exception e); + + ReactNativeConfig getReactNativeConfig(TurboModuleManager turboModuleManager); + + List getReactPackages(); +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactLifecycleStateManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactLifecycleStateManager.java new file mode 100644 index 00000000000..af857986185 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactLifecycleStateManager.java @@ -0,0 +1,90 @@ +/* + * 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.bridgeless; + +import static com.facebook.infer.annotation.ThreadConfined.UI; + +import android.app.Activity; +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.common.LifecycleState; + +@Nullsafe(Nullsafe.Mode.LOCAL) +class ReactLifecycleStateManager { + LifecycleState mState = LifecycleState.BEFORE_CREATE; + private final BridgelessReactStateTracker mBridgelessReactStateTracker; + + ReactLifecycleStateManager(BridgelessReactStateTracker bridgelessReactStateTracker) { + mBridgelessReactStateTracker = bridgelessReactStateTracker; + } + + public LifecycleState getLifecycleState() { + return mState; + } + + @ThreadConfined(UI) + public void resumeReactContextIfHostResumed( + final ReactContext currentContext, final @Nullable Activity activity) { + if (mState == LifecycleState.RESUMED) { + mBridgelessReactStateTracker.enterState("ReactContext.onHostResume()"); + currentContext.onHostResume(activity); + } + } + + @ThreadConfined(UI) + public void moveToOnHostResume( + final @Nullable ReactContext currentContext, final @Nullable Activity activity) { + if (mState == LifecycleState.RESUMED) { + return; + } + + if (currentContext != null) { + mBridgelessReactStateTracker.enterState("ReactContext.onHostResume()"); + currentContext.onHostResume(activity); + } + mState = LifecycleState.RESUMED; + } + + @ThreadConfined(UI) + public void moveToOnHostPause( + final @Nullable ReactContext currentContext, final @Nullable Activity activity) { + if (currentContext != null) { + if (mState == LifecycleState.BEFORE_CREATE) { + // TODO: Investigate if we can remove this transition. + mBridgelessReactStateTracker.enterState("ReactContext.onHostResume()"); + currentContext.onHostResume(activity); + mBridgelessReactStateTracker.enterState("ReactContext.onHostPause()"); + currentContext.onHostPause(); + } else if (mState == LifecycleState.RESUMED) { + mBridgelessReactStateTracker.enterState("ReactContext.onHostPause()"); + currentContext.onHostPause(); + } + } + + mState = LifecycleState.BEFORE_RESUME; + } + + @ThreadConfined(UI) + public void moveToOnHostDestroy(final @Nullable ReactContext currentContext) { + if (currentContext != null) { + if (mState == LifecycleState.BEFORE_RESUME) { + mBridgelessReactStateTracker.enterState("ReactContext.onHostDestroy()"); + currentContext.onHostDestroy(); + } else if (mState == LifecycleState.RESUMED) { + mBridgelessReactStateTracker.enterState("ReactContext.onHostPause()"); + currentContext.onHostPause(); + mBridgelessReactStateTracker.enterState("ReactContext.onHostDestroy()"); + currentContext.onHostDestroy(); + } + } + + mState = LifecycleState.BEFORE_CREATE; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurface.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurface.java new file mode 100644 index 00000000000..b9b5138f3be --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurface.java @@ -0,0 +1,241 @@ +/* + * 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.bridgeless; + +import android.content.Context; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.View.MeasureSpec; +import androidx.annotation.UiThread; +import bolts.Task; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.infer.annotation.ThreadSafe; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.NativeMap; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.fabric.SurfaceHandler; +import com.facebook.react.fabric.SurfaceHandlerBinding; +import com.facebook.react.modules.i18nmanager.I18nUtil; +import com.facebook.react.uimanager.events.EventDispatcher; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + +/** A class responsible for creating and rendering a full-screen React surface. */ +@Nullsafe(Nullsafe.Mode.LOCAL) +@ThreadSafe +public class ReactSurface { + private static final String TAG = "ReactSurface"; + + private final AtomicReference mSurfaceView = new AtomicReference<>(null); + + private final AtomicReference mReactHost = new AtomicReference<>(null); + + private final SurfaceHandler mSurfaceHandler; + + /** + * Surface can hold two types of context: one used to init the surface without view (e.g. during + * prerendering), and themed view context later, whenever it is available. The assumption is that + * custom theming is applied to both the same way, so changing them SHOULD NOT change the visual + * output. + */ + private Context mContext; + + public static ReactSurface createWithView( + Context context, String moduleName, @Nullable Bundle initialProps) { + ReactSurface surface = new ReactSurface(context, moduleName, initialProps); + surface.attachView(new ReactSurfaceView(context, surface)); + return surface; + } + + /** + * @param context The Android Context to use to render the ReactSurfaceView + * @param moduleName The string key used to register this surface in JS with SurfaceRegistry + * @param initialProps A Bundle of properties to be passed to the root React component + */ + public ReactSurface(Context context, String moduleName, @Nullable Bundle initialProps) { + this(new SurfaceHandlerBinding(moduleName), context); + + NativeMap nativeProps = + initialProps == null + ? new WritableNativeMap() + : (NativeMap) Arguments.fromBundle(initialProps); + mSurfaceHandler.setProps(nativeProps); + + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + mSurfaceHandler.setLayoutConstraints( + MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, MeasureSpec.AT_MOST), + 0, + 0, + doRTLSwap(context), + isRTL(context), + getPixelDensity(context)); + } + + @VisibleForTesting + ReactSurface(SurfaceHandler surfaceHandler, Context context) { + mSurfaceHandler = surfaceHandler; + mContext = context; + } + + /** + * Attach a ReactHost to the ReactSurface. + * + * @param host The ReactHost to attach. + */ + public void attach(ReactHost host) { + if (!mReactHost.compareAndSet(null, host)) { + throw new IllegalStateException("This surface is already attached to a host!"); + } + } + + /** + * Attaches a view to the surface. View can be attached only once and should not be detached + * after. + */ + public void attachView(ReactSurfaceView view) { + if (!mSurfaceView.compareAndSet(null, view)) { + throw new IllegalStateException( + "Trying to call ReactSurface.attachView(), but the view is already attached."); + } + // re: thread safety. Context can be changed only once, during view init, so view atomic above + // already guards the context update. + mContext = view.getContext(); + } + + public void updateInitProps(Bundle newProps) { + mSurfaceHandler.setProps((NativeMap) Arguments.fromBundle(newProps)); + } + + @VisibleForTesting + ReactHost getReactHost() { + // NULLSAFE_FIXME[Return Not Nullable] + return mReactHost.get(); + } + + /** Detach the ReactSurface from its ReactHost. */ + public void detach() { + mReactHost.set(null); + } + + public SurfaceHandler getSurfaceHandler() { + return mSurfaceHandler; + } + + public @Nullable ReactSurfaceView getView() { + return mSurfaceView.get(); + } + + public Task prerender() { + ReactHost host = mReactHost.get(); + if (host == null) { + return Task.forError( + new IllegalStateException( + "Trying to call ReactSurface.prerender(), but no ReactHost is attached.")); + } + return host.prerenderSurface(this); + } + + public Task start() { + if (mSurfaceView.get() == null) { + return Task.forError( + new IllegalStateException( + "Trying to call ReactSurface.start(), but view is not created.")); + } + + ReactHost host = mReactHost.get(); + if (host == null) { + return Task.forError( + new IllegalStateException( + "Trying to call ReactSurface.start(), but no ReactHost is attached.")); + } + return host.startSurface(this); + } + + public Task stop() { + ReactHost host = mReactHost.get(); + if (host == null) { + return Task.forError( + new IllegalStateException( + "Trying to call ReactSurface.stop(), but no ReactHost is attached.")); + } + return host.stopSurface(this).makeVoid(); + } + + public int getSurfaceID() { + return mSurfaceHandler.getSurfaceId(); + } + + public String getModuleName() { + return mSurfaceHandler.getModuleName(); + } + + public void clear() { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + ReactSurfaceView view = getView(); + if (view != null) { + view.removeAllViews(); + view.setId(View.NO_ID); + } + } + }); + } + + @UiThread + /* package */ synchronized void updateLayoutSpecs( + int widthMeasureSpec, int heightMeasureSpec, int offsetX, int offsetY) { + mSurfaceHandler.setLayoutConstraints( + widthMeasureSpec, + heightMeasureSpec, + offsetX, + offsetY, + doRTLSwap(mContext), + isRTL(mContext), + getPixelDensity(mContext)); + } + + /* package */ @Nullable + EventDispatcher getEventDispatcher() { + ReactHost host = mReactHost.get(); + if (host == null) { + return null; + } + return host.getEventDispatcher(); + } + + @VisibleForTesting + boolean isAttached() { + return mReactHost.get() != null; + } + + public boolean isRunning() { + return mSurfaceHandler.isRunning(); + } + + public Context getContext() { + return mContext; + } + + private static float getPixelDensity(Context context) { + return context.getResources().getDisplayMetrics().density; + } + + private static boolean isRTL(Context context) { + return I18nUtil.getInstance().isRTL(context); + } + + private static boolean doRTLSwap(Context context) { + return I18nUtil.getInstance().doLeftAndRightSwapInRTL(context); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurfaceView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurfaceView.java new file mode 100644 index 00000000000..eae0988ba7e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/ReactSurfaceView.java @@ -0,0 +1,239 @@ +/* + * 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.bridgeless; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.ReactRootView; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.JSPointerDispatcher; +import com.facebook.react.uimanager.JSTouchDispatcher; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.systrace.Systrace; + +/** A view created by {@link ReactSurface} that's responsible for rendering a React component. */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public class ReactSurfaceView extends ReactRootView { + + private static final String TAG = "ReactSurfaceView"; + + private ReactSurface mSurface; + + private final JSTouchDispatcher mJSTouchDispatcher; + private @Nullable JSPointerDispatcher mJSPointerDispatcher; + + private boolean mWasMeasured = false; + private int mWidthMeasureSpec = 0; + private int mHeightMeasureSpec = 0; + + public ReactSurfaceView(Context context, ReactSurface surface) { + super(context); + mSurface = surface; + mJSTouchDispatcher = new JSTouchDispatcher(this); + if (ReactFeatureFlags.dispatchPointerEvents) { + mJSPointerDispatcher = new JSPointerDispatcher(this); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactSurfaceView.onMeasure"); + + int width = 0; + int height = 0; + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + int childSize = + child.getLeft() + + child.getMeasuredWidth() + + child.getPaddingLeft() + + child.getPaddingRight(); + width = Math.max(width, childSize); + } + } else { + width = MeasureSpec.getSize(widthMeasureSpec); + } + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + int childSize = + child.getTop() + + child.getMeasuredHeight() + + child.getPaddingTop() + + child.getPaddingBottom(); + height = Math.max(height, childSize); + } + } else { + height = MeasureSpec.getSize(heightMeasureSpec); + } + setMeasuredDimension(width, height); + + mWasMeasured = true; + mWidthMeasureSpec = widthMeasureSpec; + mHeightMeasureSpec = heightMeasureSpec; + + Point viewportOffset = getViewportOffset(); + + mSurface.updateLayoutSpecs( + mWidthMeasureSpec, mHeightMeasureSpec, viewportOffset.x, viewportOffset.y); + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + // Call updateLayoutSpecs to update locationOnScreen offsets, in case they've changed + if (mWasMeasured && changed) { + Point viewportOffset = getViewportOffset(); + mSurface.updateLayoutSpecs( + mWidthMeasureSpec, mHeightMeasureSpec, viewportOffset.x, viewportOffset.y); + } + } + + private Point getViewportOffset() { + int[] locationOnScreen = new int[2]; + getLocationOnScreen(locationOnScreen); + + // we need to subtract visibleWindowCoords - to subtract possible window insets, split + // screen or multi window + Rect visibleWindowFrame = new Rect(); + getWindowVisibleDisplayFrame(visibleWindowFrame); + locationOnScreen[0] -= visibleWindowFrame.left; + locationOnScreen[1] -= visibleWindowFrame.top; + + return new Point(locationOnScreen[0], locationOnScreen[1]); + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + // Override in order to still receive events to onInterceptTouchEvent even when some other + // views disallow that, but propagate it up the tree if possible. + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + /** + * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called + * from the child's onTouchIntercepted implementation. + */ + @Override + public void onChildStartedNativeGesture(MotionEvent ev) { + if (mJSTouchDispatcher != null && mSurface.getEventDispatcher() != null) { + mJSTouchDispatcher.onChildStartedNativeGesture(ev, mSurface.getEventDispatcher()); + } + } + + /** + * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called + * from the child's onTouchIntercepted implementation. + */ + @Override + public void onChildStartedNativeGesture(View childView, MotionEvent ev) { + onChildStartedNativeGesture(ev); + } + + @Override + public void onChildEndedNativeGesture(View childView, MotionEvent ev) { + if (mJSTouchDispatcher != null && mSurface.getEventDispatcher() != null) { + mJSTouchDispatcher.onChildEndedNativeGesture(ev, mSurface.getEventDispatcher()); + } + } + + @Override + public void handleException(Throwable t) { + if (mSurface.getReactHost() != null) { + String errorMessage = t.getMessage() == null ? "" : t.getMessage(); + Exception e = new IllegalViewOperationException(errorMessage, this, t); + mSurface.getReactHost().handleException(e); + } + } + + @Override + public void setIsFabric(boolean isFabric) { + // This surface view is always on Fabric regardless. + super.setIsFabric(true); + } + + @Override + public @UIManagerType int getUIManagerType() { + // This surface view is always on Fabric. + return UIManagerType.FABRIC; + } + + @Override + protected void dispatchJSTouchEvent(MotionEvent event) { + if (mJSTouchDispatcher == null) { + FLog.w(TAG, "Unable to dispatch touch events to JS before the dispatcher is available"); + return; + } + EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); + if (eventDispatcher != null) { + mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher); + } else { + FLog.w( + TAG, "Unable to dispatch touch events to JS as the React instance has not been attached"); + } + } + + @Override + protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { + if (mJSPointerDispatcher == null) { + if (!ReactFeatureFlags.dispatchPointerEvents) { + return; + } + FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available"); + return; + } + EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); + if (eventDispatcher != null) { + mJSPointerDispatcher.handleMotionEvent(event, eventDispatcher, isCapture); + } else { + FLog.w( + TAG, + "Unable to dispatch pointer events to JS as the React instance has not been attached"); + } + } + + @Override + public boolean hasActiveReactContext() { + return mSurface.isAttached() && mSurface.getReactHost().getCurrentReactContext() != null; + } + + @Override + public boolean hasActiveReactInstance() { + return mSurface.isAttached() && mSurface.getReactHost().isInstanceInitialized(); + } + + @Override + public @Nullable ReactContext getCurrentReactContext() { + if (mSurface.isAttached()) { + return mSurface.getReactHost().getCurrentReactContext(); + } + return null; + } + + @Override + public boolean isViewAttachedToReactInstance() { + return mSurface.isAttached(); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/exceptionmanager/ReactJsExceptionHandler.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/exceptionmanager/ReactJsExceptionHandler.java new file mode 100644 index 00000000000..470bf037bbb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/exceptionmanager/ReactJsExceptionHandler.java @@ -0,0 +1,17 @@ +/* + * 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.bridgeless.exceptionmanager; + +import com.facebook.proguard.annotations.DoNotStripAny; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; + +@DoNotStripAny +public interface ReactJsExceptionHandler { + + public void reportJsException(ReadableMapBuffer errorMap); +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/hermes/HermesInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/hermes/HermesInstance.kt new file mode 100644 index 00000000000..6e1ab8e8baa --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridgeless/hermes/HermesInstance.kt @@ -0,0 +1,34 @@ +/* + * 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.bridgeless.hermes + +import com.facebook.common.process.ProcessName +import com.facebook.jni.HybridData +import com.facebook.jni.annotations.DoNotStrip +import com.facebook.jsi.mdcd.HermesCodeCoverage +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridgeless.JSEngineInstance +import com.facebook.soloader.SoLoader +import kotlin.jvm.JvmStatic + +class HermesInstance(context: ReactApplicationContext) : JSEngineInstance(initHybrid()!!) { + + companion object { + @JvmStatic @DoNotStrip protected external fun initHybrid(): HybridData? + + init { + SoLoader.loadLibrary("hermesinstancejni") + } + } + + init { + // Initialize Code Coverage tracing for select usecases, like E2E coverage and Hermes MDCD + val processName = ProcessName.current().fullName + HermesCodeCoverage.initialize(context, processName.orEmpty()) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/hermes/jni/JHermesInstance.h b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/hermes/jni/JHermesInstance.h index 8f972c6e7a1..13a427057bc 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/hermes/jni/JHermesInstance.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/hermes/jni/JHermesInstance.h @@ -1,4 +1,9 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * 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. + */ #pragma once @@ -19,7 +24,7 @@ class JHermesInstance : public jni::HybridClass { public: static constexpr auto kJavaDescriptor = - "Lcom/facebook/venice/hermes/HermesInstance;"; + "Lcom/facebook/react/bridgeless/hermes/HermesInstance;"; static jni::local_ref initHybrid(jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSEngineInstance.h b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSEngineInstance.h index 14cdcc7109e..ee166fb6b17 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSEngineInstance.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSEngineInstance.h @@ -1,4 +1,9 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * 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. + */ #pragma once @@ -14,7 +19,7 @@ class JJSEngineInstance : public jni::HybridClass, public JSEngineInstance { public: static auto constexpr kJavaDescriptor = - "Lcom/facebook/venice/JSEngineInstance;"; + "Lcom/facebook/react/bridgeless/JSEngineInstance;"; private: friend HybridBase; diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSTimerExecutor.h b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSTimerExecutor.h index 3356432c99a..d5a9904c9fe 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSTimerExecutor.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JJSTimerExecutor.h @@ -1,4 +1,9 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * 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. + */ #pragma once @@ -16,7 +21,7 @@ class JJSTimerExecutor : public jni::HybridClass { JJSTimerExecutor() = default; constexpr static auto kJavaDescriptor = - "Lcom/facebook/venice/JSTimerExecutor;"; + "Lcom/facebook/react/bridgeless/JSTimerExecutor;"; static void registerNatives(); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactExceptionManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactExceptionManager.h index 2d5886010f6..7f16ec9ca7e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactExceptionManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactExceptionManager.h @@ -1,4 +1,9 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * 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. + */ #pragma once @@ -13,7 +18,7 @@ class JReactExceptionManager : public facebook::jni::JavaClass { public: static auto constexpr kJavaDescriptor = - "Lcom/facebook/venice/exceptionmanager/ReactJsExceptionHandler;"; + "Lcom/facebook/react/bridgeless/exceptionmanager/ReactJsExceptionHandler;"; void reportJsException(const JReadableMapBuffer::javaobject errorMapBuffer); }; diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactInstance.h b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactInstance.h index 049c2dc1f21..3ab71e7b2fa 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactInstance.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/bridgeless/jni/JReactInstance.h @@ -1,4 +1,9 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * 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. + */ #pragma once @@ -26,7 +31,8 @@ namespace react { class JReactInstance : public jni::HybridClass { public: - constexpr static auto kJavaDescriptor = "Lcom/facebook/venice/ReactInstance;"; + constexpr static auto kJavaDescriptor = + "Lcom/facebook/react/bridgeless/ReactInstance;"; static jni::local_ref initHybrid( jni::alias_ref, diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/BridgelessReactContextTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/BridgelessReactContextTest.java new file mode 100644 index 00000000000..3965a2bbf58 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/BridgelessReactContextTest.java @@ -0,0 +1,71 @@ +/* + * 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.bridgeless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +import android.app.Activity; +import android.content.Context; +import com.facebook.react.bridge.JSIModuleType; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.testing.robolectric.v4.WithTestDefaultsRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.robolectric.Robolectric; + +/** Tests {@link BridgelessReactContext} */ +@RunWith(WithTestDefaultsRunner.class) +public class BridgelessReactContextTest { + + private Context mContext; + private ReactHost mReactHost; + private BridgelessReactContext mBridgelessReactContext; + + @Before + public void setUp() { + mContext = Robolectric.buildActivity(Activity.class).create().get(); + mReactHost = Mockito.mock(ReactHost.class); + mBridgelessReactContext = new BridgelessReactContext(mContext, mReactHost); + } + + @Test + public void getNativeModuleTest() { + UIManagerModule mUiManagerModule = Mockito.mock(UIManagerModule.class); + doReturn(mUiManagerModule) + .when(mReactHost) + .getNativeModule(ArgumentMatchers.>any()); + + UIManagerModule uiManagerModule = + mBridgelessReactContext.getNativeModule(UIManagerModule.class); + + assertThat(uiManagerModule).isEqualTo(mUiManagerModule); + } + + @Test(expected = UnsupportedOperationException.class) + public void getJSIModule_throwsException() { + mBridgelessReactContext.getJSIModule(JSIModuleType.TurboModuleManager); + } + + @Test + public void getJSIModuleTest() { + FabricUIManager fabricUiManager = Mockito.mock(FabricUIManager.class); + doReturn(fabricUiManager).when(mReactHost).getUIManager(); + assertThat(mBridgelessReactContext.getJSIModule(JSIModuleType.UIManager)) + .isEqualTo(fabricUiManager); + } + + @Test(expected = UnsupportedOperationException.class) + public void getCatalystInstance_throwsException() { + mBridgelessReactContext.getCatalystInstance(); + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactHostTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactHostTest.java new file mode 100644 index 00000000000..47c4bf7c9cd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactHostTest.java @@ -0,0 +1,131 @@ +/* + * 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.bridgeless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +import android.app.Activity; +import bolts.TaskCompletionSource; +import com.facebook.base.applicationholder.ApplicationHolder; +import com.facebook.react.MemoryPressureRouter; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.MemoryPressureListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.UIManager; +import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; +import com.facebook.react.uimanager.events.BlackHoleEventDispatcher; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.testing.robolectric.RobolectricTestUtil; +import com.facebook.testing.robolectric.v4.WithTestDefaultsRunner; +import com.facebook.ultralight.testing.MockitoWithUltralightAutoMockSupport; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.Robolectric; +import org.robolectric.android.controller.ActivityController; + +/** Tests {@linkcom.facebook.react.bridgeless.ReactHost} */ +@RunWith(WithTestDefaultsRunner.class) +@PowerMockIgnore({ + "org.mockito.*", + "org.robolectric.*", + "android.*", + "androidx.*", + "javax.net.ssl.*" +}) +@PrepareForTest({ReactHost.class}) +public class ReactHostTest extends MockitoWithUltralightAutoMockSupport { + + private ReactInstanceDelegate mReactInstanceDelegate; + private ReactInstance mReactInstance; + private MemoryPressureRouter mMemoryPressureRouter; + private BridgelessDevSupportManager mDevSupportManager; + private JSBundleLoader mJSBundleLoader; + private ReactHost mReactHost; + private ActivityController mActivityController; + + @Rule public PowerMockRule rule = new PowerMockRule(); + + @Before + public void setUp() throws Exception { + initMocks(this); + + try { + ApplicationHolder.set(RobolectricTestUtil.getInstance().getApplication()); + } catch (IllegalStateException e) { + // Do nothing, we're already initialized. + } + + mActivityController = Robolectric.buildActivity(Activity.class).create().start().resume(); + initializeNiceMockInjector(mActivityController.get()); + + mReactInstanceDelegate = mock(ReactInstanceDelegate.class); + mReactInstance = mock(ReactInstance.class); + mMemoryPressureRouter = mock(MemoryPressureRouter.class); + mDevSupportManager = mock(BridgelessDevSupportManager.class); + mJSBundleLoader = mock(JSBundleLoader.class); + + whenNew(ReactInstance.class).withAnyArguments().thenReturn(mReactInstance); + whenNew(MemoryPressureRouter.class).withAnyArguments().thenReturn(mMemoryPressureRouter); + whenNew(BridgelessDevSupportManager.class).withAnyArguments().thenReturn(mDevSupportManager); + + doReturn(mJSBundleLoader) + .when(mReactInstanceDelegate) + .getJSBundleLoader(ArgumentMatchers.any()); + + mReactHost = + new ReactHost( + mActivityController.get().getApplication(), mReactInstanceDelegate, false, null, false); + } + + @Test + public void getEventDispatcher_returnsBlackHoleEventDispatcher() { + EventDispatcher eventDispatcher = mReactHost.getEventDispatcher(); + assertThat(eventDispatcher).isInstanceOf(BlackHoleEventDispatcher.class); + } + + @Test + public void getUIManager_returnsNullIfNoInstance() { + UIManager uiManager = mReactHost.getUIManager(); + assertThat(uiManager).isNull(); + } + + @Ignore("FIXME") + public void testGetDevSupportManager() { + assertThat(mReactHost.getDevSupportManager()).isEqualTo(mDevSupportManager); + } + + @Ignore("waitForCompletion is locking the test thread making the entire venice tests to timeout") + public void testPreload() throws Exception { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + taskCompletionSource.setResult(true); + whenNew(TaskCompletionSource.class).withAnyArguments().thenReturn(taskCompletionSource); + doNothing().when(mDevSupportManager).isPackagerRunning(any(PackagerStatusCallback.class)); + + assertThat(mReactHost.isInstanceInitialized()).isFalse(); + + mReactHost.preload().waitForCompletion(); + + assertThat(mReactHost.isInstanceInitialized()).isTrue(); + assertThat(mReactHost.getCurrentReactContext()).isNotNull(); + verify(mMemoryPressureRouter).addMemoryPressureListener((MemoryPressureListener) any()); + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactSurfaceTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactSurfaceTest.java new file mode 100644 index 00000000000..1c6c748810b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridgeless/ReactSurfaceTest.java @@ -0,0 +1,251 @@ +/* + * 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.bridgeless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.robolectric.shadows.ShadowInstrumentation.getInstrumentation; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import bolts.Task; +import com.facebook.react.bridge.NativeMap; +import com.facebook.react.fabric.SurfaceHandler; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.testing.robolectric.v4.WithTestDefaultsRunner; +import java.util.concurrent.Callable; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; + +@RunWith(WithTestDefaultsRunner.class) +public class ReactSurfaceTest { + @Mock ReactInstanceDelegate mReactInstanceDelegate; + @Mock EventDispatcher mEventDispatcher; + + private ReactHost mReactHost; + private Context mContext; + private ReactSurface mReactSurface; + private TestSurfaceHandler mSurfaceHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = Robolectric.buildActivity(Activity.class).create().get(); + + mReactHost = spy(new ReactHost(mContext, mReactInstanceDelegate, false, null, false)); + doAnswer(mockedStartSurface()).when(mReactHost).startSurface(any(ReactSurface.class)); + doAnswer(mockedStartSurface()).when(mReactHost).prerenderSurface(any(ReactSurface.class)); + doAnswer(mockedStopSurface()).when(mReactHost).stopSurface(any(ReactSurface.class)); + doReturn(mEventDispatcher).when(mReactHost).getEventDispatcher(); + + mSurfaceHandler = new TestSurfaceHandler(); + mReactSurface = new ReactSurface(mSurfaceHandler, mContext); + mReactSurface.attachView(new ReactSurfaceView(mContext, mReactSurface)); + } + + @Test + public void testAttach() { + assertThat(mReactSurface.getReactHost()).isNull(); + mReactSurface.attach(mReactHost); + assertThat(mReactSurface.getReactHost()).isEqualTo(mReactHost); + assertThat(mReactSurface.isAttached()).isTrue(); + } + + @Test(expected = IllegalStateException.class) + public void testAttachThrowExeption() { + mReactSurface.attach(mReactHost); + mReactSurface.attach(mReactHost); + } + + @Test + public void testPrerender() throws InterruptedException { + mReactSurface.attach(mReactHost); + Task task = mReactSurface.prerender(); + task.waitForCompletion(); + + verify(mReactHost).prerenderSurface(mReactSurface); + assertThat(mSurfaceHandler.isRunning).isTrue(); + } + + @Test + public void testStart() throws InterruptedException { + mReactSurface.attach(mReactHost); + assertThat(mReactHost.isSurfaceAttached(mReactSurface)).isFalse(); + Task task = mReactSurface.start(); + task.waitForCompletion(); + + verify(mReactHost).startSurface(mReactSurface); + assertThat(mSurfaceHandler.isRunning).isTrue(); + } + + @Test + public void testStop() throws InterruptedException { + mReactSurface.attach(mReactHost); + + Task task = mReactSurface.start(); + task.waitForCompletion(); + + task = mReactSurface.stop(); + task.waitForCompletion(); + + verify(mReactHost).stopSurface(mReactSurface); + } + + @Test + public void testClear() { + mReactSurface.getView().addView(new View(mContext)); + + mReactSurface.clear(); + + getInstrumentation().waitForIdleSync(); + + assertThat(mReactSurface.getView().getId()).isEqualTo(View.NO_ID); + assertThat(mReactSurface.getView().getChildCount()).isEqualTo(0); + } + + @Test + public void testGetLayoutSpecs() { + int measureSpecWidth = Integer.MAX_VALUE; + int measureSpecHeight = Integer.MIN_VALUE; + + assertThat(mSurfaceHandler.mWidthMeasureSpec).isNotEqualTo(measureSpecWidth); + assertThat(mSurfaceHandler.mHeightMeasureSpec).isNotEqualTo(measureSpecHeight); + + mReactSurface.attach(mReactHost); + mReactSurface.updateLayoutSpecs(measureSpecWidth, measureSpecHeight, 2, 3); + + assertThat(mSurfaceHandler.mWidthMeasureSpec).isEqualTo(measureSpecWidth); + assertThat(mSurfaceHandler.mHeightMeasureSpec).isEqualTo(measureSpecHeight); + } + + @Test + public void testGetEventDispatcher() { + mReactSurface.attach(mReactHost); + assertThat(mReactSurface.getEventDispatcher()).isEqualTo(mEventDispatcher); + } + + @Test + public void testStartStopHandlerCalls() throws InterruptedException { + mReactSurface.attach(mReactHost); + + assertThat(mReactSurface.isRunning()).isFalse(); + + mReactSurface.start().waitForCompletion(); + + assertThat(mReactSurface.isRunning()).isTrue(); + + mReactSurface.stop().waitForCompletion(); + + assertThat(mReactSurface.isRunning()).isFalse(); + } + + private Answer> mockedStartSurface() { + return new Answer>() { + @Override + public Task answer(InvocationOnMock invocation) { + return Task.call( + new Callable() { + @Override + public Void call() { + mSurfaceHandler.start(); + return null; + } + }); + } + }; + } + + private Answer> mockedStopSurface() { + return new Answer>() { + @Override + public Task answer(InvocationOnMock invocation) { + return Task.call( + new Callable() { + @Override + public Boolean call() { + mSurfaceHandler.stop(); + return true; + } + }); + } + }; + } + + static class TestSurfaceHandler implements SurfaceHandler { + private boolean isRunning = false; + private int mSurfaceId = 0; + + private int mHeightMeasureSpec = 0; + private int mWidthMeasureSpec = 0; + + @Override + public void start() { + isRunning = true; + } + + @Override + public void stop() { + isRunning = false; + } + + @Override + public int getSurfaceId() { + return mSurfaceId; + } + + @Override + public void setSurfaceId(int surfaceId) { + mSurfaceId = surfaceId; + } + + @Override + public boolean isRunning() { + return isRunning; + } + + @Override + public String getModuleName() { + return null; + } + + @Override + public void setMountable(boolean mountable) { + // no-op + } + + @Override + public void setLayoutConstraints( + int widthMeasureSpec, + int heightMeasureSpec, + int offsetX, + int offsetY, + boolean doLeftAndRightSwapInRTL, + boolean isRTL, + float pixelDensity) { + mWidthMeasureSpec = widthMeasureSpec; + mHeightMeasureSpec = heightMeasureSpec; + } + + @Override + public void setProps(NativeMap props) { + // no-op + } + } +}