diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index bbfe2ccf38f..97ee3b9263a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3222,9 +3222,9 @@ public abstract class com/facebook/react/runtime/JSRuntimeFactory { public fun (Lcom/facebook/jni/HybridData;)V } -public class com/facebook/react/runtime/ReactHostImpl : com/facebook/react/ReactHost { - public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZ)V +public final class com/facebook/react/runtime/ReactHostImpl : com/facebook/react/ReactHost { public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;)V + public synthetic fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZLcom/facebook/react/devsupport/DevSupportManagerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZZ)V public fun addBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V public fun addReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressureListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressureListener.kt index 0e5ef3aff09..9f1d7bbd590 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressureListener.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressureListener.kt @@ -8,7 +8,7 @@ package com.facebook.react.bridge /** Listener interface for memory pressure events. */ -public interface MemoryPressureListener { +public fun interface MemoryPressureListener { /** Called when the system generates a memory warning. */ public fun handleMemoryPressure(level: Int) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessAtomicRef.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessAtomicRef.kt index 630d0f9479f..64d7e2dc0ac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessAtomicRef.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessAtomicRef.kt @@ -14,7 +14,7 @@ internal class BridgelessAtomicRef( @field:Volatile @get:Synchronized @get:JvmName("getNullable") var value: T? = null ) { - internal interface Provider { + internal fun interface Provider { fun get(): T } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt index 85db3f42ccf..98327c8d3b7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessCatalystInstance.kt @@ -24,6 +24,7 @@ import com.facebook.react.bridge.RuntimeExecutor import com.facebook.react.bridge.RuntimeScheduler import com.facebook.react.bridge.UIManager import com.facebook.react.bridge.queue.ReactQueueConfiguration +import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.annotations.internal.LegacyArchitecture import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger @@ -35,6 +36,7 @@ import com.facebook.react.turbomodule.core.interfaces.NativeMethodCallInvokerHol message = "This class is deprecated, please to migrate to new architecture using [com.facebook.react.defaults.DefaultReactHost] instead.") @LegacyArchitecture +@FrameworkAPI internal class BridgelessCatalystInstance(private val reactHost: ReactHostImpl) : CatalystInstance { override fun handleMemoryPressure(level: Int) { @@ -94,13 +96,13 @@ internal class BridgelessCatalystInstance(private val reactHost: ReactHostImpl) @get:Deprecated("Deprecated in Java") public override val javaScriptContextHolder: JavaScriptContextHolder - get() = reactHost.getJavaScriptContextHolder()!! + get() = reactHost.javaScriptContextHolder!! @Suppress("INAPPLICABLE_JVM_NAME") @get:Deprecated("Deprecated in Java") @get:JvmName("getJSCallInvokerHolder") // This is needed to keep backward compatibility public override val jsCallInvokerHolder: CallInvokerHolder - get() = reactHost.getJSCallInvokerHolder()!! + get() = reactHost.jsCallInvokerHolder!! public override val nativeMethodCallInvokerHolder: NativeMethodCallInvokerHolder get() = @@ -117,13 +119,13 @@ internal class BridgelessCatalystInstance(private val reactHost: ReactHostImpl) reactHost.getNativeModule(moduleName) public override val nativeModules: Collection - get() = reactHost.getNativeModules() + get() = reactHost.nativeModules public override val reactQueueConfiguration: ReactQueueConfiguration get() = reactHost.reactQueueConfiguration!! public override val runtimeExecutor: RuntimeExecutor? - get() = reactHost.getRuntimeExecutor() + get() = reactHost.runtimeExecutor public override val runtimeScheduler: RuntimeScheduler get() = throw UnsupportedOperationException("Unimplemented method 'getRuntimeScheduler'") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt index 61de1a62cc1..b2446a837b4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt @@ -63,6 +63,7 @@ internal class BridgelessReactContext(context: Context, private val reactHost: R @Deprecated("This method is deprecated, please use UIManagerHelper.getUIManager() instead.") override fun getFabricUIManager(): UIManager? = reactHost.uiManager + @OptIn(FrameworkAPI::class) override fun getCatalystInstance(): CatalystInstance { if (ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE) { throw UnsupportedOperationException( @@ -136,7 +137,7 @@ internal class BridgelessReactContext(context: Context, private val reactHost: R override fun hasNativeModule(nativeModuleInterface: Class): Boolean = reactHost.hasNativeModule(nativeModuleInterface) - override fun getNativeModules(): MutableCollection = reactHost.nativeModules + override fun getNativeModules(): Collection = reactHost.nativeModules override fun getNativeModule(nativeModuleInterface: Class): T? = reactHost.getNativeModule(nativeModuleInterface) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java deleted file mode 100644 index 38b00dac2cd..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java +++ /dev/null @@ -1,1825 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.runtime; - -import static com.facebook.infer.annotation.Assertions.assertNotNull; -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 android.content.Intent; -import android.net.Uri; -import android.nfc.NfcAdapter; -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -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.proguard.annotations.DoNotStrip; -import com.facebook.react.MemoryPressureRouter; -import com.facebook.react.ReactHost; -import com.facebook.react.ReactInstanceEventListener; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.JSBundleLoader; -import com.facebook.react.bridge.JavaScriptContextHolder; -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.RuntimeExecutor; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.bridge.queue.ReactQueueConfiguration; -import com.facebook.react.common.LifecycleState; -import com.facebook.react.common.build.ReactBuildConfig; -import com.facebook.react.devsupport.DefaultDevSupportManagerFactory; -import com.facebook.react.devsupport.DevSupportManagerBase; -import com.facebook.react.devsupport.DevSupportManagerFactory; -import com.facebook.react.devsupport.InspectorFlags; -import com.facebook.react.devsupport.inspector.InspectorNetworkHelper; -import com.facebook.react.devsupport.inspector.InspectorNetworkRequestListener; -import com.facebook.react.devsupport.interfaces.BundleLoadCallback; -import com.facebook.react.devsupport.interfaces.DevSupportManager; -import com.facebook.react.devsupport.interfaces.DevSupportManager.PausedInDebuggerOverlayCommandListener; -import com.facebook.react.fabric.ComponentFactory; -import com.facebook.react.fabric.FabricUIManager; -import com.facebook.react.interfaces.TaskInterface; -import com.facebook.react.interfaces.fabric.ReactSurface; -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; -import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags; -import com.facebook.react.modules.appearance.AppearanceModule; -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; -import com.facebook.react.runtime.internal.bolts.Continuation; -import com.facebook.react.runtime.internal.bolts.Task; -import com.facebook.react.runtime.internal.bolts.TaskCompletionSource; -import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder; -import com.facebook.react.uimanager.DisplayMetricsHolder; -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.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import kotlin.Unit; -import kotlin.jvm.functions.Function0; -import kotlin.jvm.functions.Function1; - -/** - * 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 - */ -@DoNotStrip -@ThreadSafe -@Nullsafe(Nullsafe.Mode.LOCAL) -public class ReactHostImpl implements ReactHost { - private static final String TAG = "ReactHost"; - private static final int BRIDGELESS_MARKER_INSTANCE_KEY = 1; - private static final AtomicInteger mCounter = new AtomicInteger(0); - - private final Context mContext; - private final ReactHostDelegate mReactHostDelegate; - private final ComponentFactory mComponentFactory; - private DevSupportManager mDevSupportManager; - private final Executor mBGExecutor; - private final Executor mUIExecutor; - private final Set mAttachedSurfaces = new HashSet<>(); - private final MemoryPressureRouter mMemoryPressureRouter; - private final boolean mAllowPackagerServerAccess; - private final boolean mUseDevSupport; - - // todo: T192399917 This no longer needs to store the react instance - private final BridgelessAtomicRef> mCreateReactInstanceTaskRef = - new BridgelessAtomicRef<>(Task.forResult(null)); - private @Nullable ReactInstance mReactInstance; - - private final BridgelessAtomicRef mBridgelessReactContextRef = - new BridgelessAtomicRef<>(); - - private final AtomicReference mActivity = new AtomicReference<>(); - private final AtomicReference> mLastUsedActivity = - new AtomicReference<>(new WeakReference<>(null)); - private final BridgelessReactStateTracker mBridgelessReactStateTracker = - new BridgelessReactStateTracker(ReactBuildConfig.DEBUG); - private final ReactLifecycleStateManager mReactLifecycleStateManager = - new ReactLifecycleStateManager(mBridgelessReactStateTracker); - private final int mId = mCounter.getAndIncrement(); - private @Nullable MemoryPressureListener mMemoryPressureListener; - private @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler; - - private final List mReactInstanceEventListeners = - new CopyOnWriteArrayList<>(); - private final List> mBeforeDestroyListeners = new CopyOnWriteArrayList<>(); - - private @Nullable ReactHostInspectorTarget mReactHostInspectorTarget; - - private volatile boolean mHostInvalidated = false; - - public ReactHostImpl( - Context context, - ReactHostDelegate delegate, - ComponentFactory componentFactory, - boolean allowPackagerServerAccess, - boolean useDevSupport) { - this( - context, - delegate, - componentFactory, - Executors.newSingleThreadExecutor(), - Task.UI_THREAD_EXECUTOR, - allowPackagerServerAccess, - useDevSupport); - } - - public ReactHostImpl( - Context context, - ReactHostDelegate delegate, - ComponentFactory componentFactory, - Executor bgExecutor, - Executor uiExecutor, - boolean allowPackagerServerAccess, - boolean useDevSupport) { - this( - context, - delegate, - componentFactory, - bgExecutor, - uiExecutor, - allowPackagerServerAccess, - useDevSupport, - null); - } - - public ReactHostImpl( - Context context, - ReactHostDelegate delegate, - ComponentFactory componentFactory, - Executor bgExecutor, - Executor uiExecutor, - boolean allowPackagerServerAccess, - boolean useDevSupport, - @Nullable DevSupportManagerFactory devSupportManagerFactory) { - mContext = context; - mReactHostDelegate = delegate; - mComponentFactory = componentFactory; - mBGExecutor = bgExecutor; - mUIExecutor = uiExecutor; - mMemoryPressureRouter = new MemoryPressureRouter(context); - mAllowPackagerServerAccess = allowPackagerServerAccess; - mUseDevSupport = useDevSupport; - if (devSupportManagerFactory == null) { - devSupportManagerFactory = new DefaultDevSupportManagerFactory(); - } - - mDevSupportManager = - devSupportManagerFactory.create( - /* applicationContext */ context.getApplicationContext(), - /* reactInstanceManagerHelper */ new ReactHostImplDevHelper(ReactHostImpl.this), - /* packagerPathForJSBundleName */ mReactHostDelegate.getJsMainModulePath(), - /* enableOnCreate */ true, - /* redBoxHandler */ null, - /* devBundleDownloadListener */ null, - /* minNumShakes */ 2, - /* customPackagerCommandHandlers */ null, - /* surfaceDelegateFactory */ null, - /* devLoadingViewManager */ null, - /* pausedInDebuggerOverlayManager */ null, - mUseDevSupport); - } - - @Override - 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. - */ - @Override - public TaskInterface start() { - return Task.call(this::getOrCreateStartTask, mBGExecutor); - } - - /** Initialize and run a React Native surface in a background without mounting real views. */ - /* package */ - TaskInterface prerenderSurface(final ReactSurfaceImpl surface) { - final String method = "prerenderSurface(surfaceId = " + surface.getSurfaceID() + ")"; - log(method, "Schedule"); - - attachSurface(surface); - return callAfterGetOrCreateReactInstance( - method, - reactInstance -> { - log(method, "Execute"); - reactInstance.prerenderSurface(surface); - }, - mBGExecutor); - } - - /** - * 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. - */ - /** package */ - TaskInterface startSurface(final ReactSurfaceImpl surface) { - final String method = "startSurface(surfaceId = " + surface.getSurfaceID() + ")"; - log(method, "Schedule"); - - attachSurface(surface); - return callAfterGetOrCreateReactInstance( - method, - reactInstance -> { - log(method, "Execute"); - reactInstance.startSurface(surface); - }, - mBGExecutor); - } - - /** - * Stop rendering a React Native surface. - * - * @param surface The surface to stop - * @return A Task that will complete when stopSurface has been called. - */ - /** package */ - TaskInterface stopSurface(final ReactSurfaceImpl surface) { - final String method = "stopSurface(surfaceId = " + surface.getSurfaceID() + ")"; - log(method, "Schedule"); - - detachSurface(surface); - return callWithExistingReactInstance( - method, - reactInstance -> { - log(method, "Execute"); - reactInstance.stopSurface(surface); - }, - mBGExecutor) - .makeVoid(); - } - - /** - * To be called when the host activity is resumed. - * - * @param activity The host activity - */ - @ThreadConfined(UI) - @Override - public void onHostResume( - final @Nullable Activity activity, - @Nullable DefaultHardwareBackBtnHandler defaultBackButtonImpl) { - mDefaultHardwareBackBtnHandler = defaultBackButtonImpl; - onHostResume(activity); - } - - @ThreadConfined(UI) - @Override - public void onHostResume(final @Nullable Activity activity) { - final String method = "onHostResume(activity)"; - log(method); - - setCurrentActivity(activity); - ReactContext currentContext = getCurrentReactContext(); - - maybeEnableDevSupport(true); - mReactLifecycleStateManager.moveToOnHostResume(currentContext, getCurrentActivity()); - } - - @ThreadConfined(UI) - @Override - public void onHostLeaveHint(final @Nullable Activity activity) { - final String method = "onUserLeaveHint(activity)"; - log(method); - - ReactContext currentContext = getCurrentReactContext(); - if (currentContext != null) { - currentContext.onUserLeaveHint(activity); - } - } - - @ThreadConfined(UI) - @Override - public void onHostPause(final @Nullable Activity activity) { - final String method = "onHostPause(activity)"; - log(method); - - ReactContext currentContext = getCurrentReactContext(); - - Activity currentActivity = getCurrentActivity(); - 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); - } - - maybeEnableDevSupport(false); - mDefaultHardwareBackBtnHandler = null; - mReactLifecycleStateManager.moveToOnHostPause(currentContext, currentActivity); - } - - /** To be called when the host activity is paused. */ - @ThreadConfined(UI) - @Override - public void onHostPause() { - final String method = "onHostPause()"; - log(method); - - ReactContext currentContext = getCurrentReactContext(); - - maybeEnableDevSupport(false); - mDefaultHardwareBackBtnHandler = null; - mReactLifecycleStateManager.moveToOnHostPause(currentContext, getCurrentActivity()); - } - - /** To be called when the host activity is destroyed. */ - @ThreadConfined(UI) - @Override - public void onHostDestroy() { - final String method = "onHostDestroy()"; - log(method); - - maybeEnableDevSupport(false); - moveToHostDestroy(getCurrentReactContext()); - } - - @ThreadConfined(UI) - @Override - public void onHostDestroy(@Nullable Activity activity) { - final String method = "onHostDestroy(activity)"; - log(method); - - Activity currentActivity = getCurrentActivity(); - - if (currentActivity == activity) { - maybeEnableDevSupport(false); - moveToHostDestroy(getCurrentReactContext()); - } - } - - private void maybeEnableDevSupport(boolean enabled) { - if (mUseDevSupport) { - mDevSupportManager.setDevSupportEnabled(enabled); - } - } - - /** - * Returns current ReactContext which could be nullable if ReactInstance hasn't been created. - * - * @return The {@link BridgelessReactContext} associated with ReactInstance. - */ - @Override - public @Nullable ReactContext getCurrentReactContext() { - return mBridgelessReactContextRef.getNullable(); - } - - @Override - public DevSupportManager getDevSupportManager() { - return assertNotNull(mDevSupportManager); - } - - @Override - public ReactSurface createSurface( - Context context, String moduleName, @Nullable Bundle initialProps) { - ReactSurfaceImpl surface = new ReactSurfaceImpl(context, moduleName, initialProps); - ReactSurfaceView surfaceView = new ReactSurfaceView(context, surface); - surfaceView.setShouldLogContentAppeared(true); - surface.attachView(surfaceView); - surface.attach(this); - return surface; - } - - @Override - public MemoryPressureRouter getMemoryPressureRouter() { - return mMemoryPressureRouter; - } - - /* package */ boolean isInstanceInitialized() { - return mReactInstance != null; - } - - @ThreadConfined(UI) - @Override - public boolean onBackPressed() { - UiThreadUtil.assertOnUiThread(); - final ReactInstance reactInstance = mReactInstance; - if (reactInstance == null) { - return false; - } - - DeviceEventManagerModule deviceEventManagerModule = - reactInstance.getNativeModule(DeviceEventManagerModule.class); - if (deviceEventManagerModule == null) { - return false; - } - - deviceEventManagerModule.emitHardwareBackPressed(); - return true; - } - - public @Nullable ReactQueueConfiguration getReactQueueConfiguration() { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.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); - } - - /** - * Entrypoint to reload the ReactInstance. If the ReactInstance is destroying, will wait until - * destroy is finished, before reloading. - * - * @param reason {@link String} describing why ReactHost is being reloaded (e.g. js error, user - * tap on reload button) - * @return A task that completes when React Native reloads - */ - @Override - public TaskInterface reload(String reason) { - final String method = "reload()"; - return Task.call( - () -> { - Task reloadTask = null; - if (mDestroyTask != null) { - log(method, "Waiting for destroy to finish, before reloading React Native."); - reloadTask = - mDestroyTask - .continueWithTask(task -> getOrCreateReloadTask(reason), mBGExecutor) - .makeVoid(); - } else { - reloadTask = getOrCreateReloadTask(reason).makeVoid(); - } - - return reloadTask.continueWithTask( - task -> { - if (task.isFaulted()) { - final Exception ex = task.getError(); - Assertions.assertNotNull(ex, "Reload failed without an exception"); - if (mUseDevSupport) { - mDevSupportManager.handleException(ex); - } else { - mReactHostDelegate.handleInstanceException(ex); - } - return getOrCreateDestroyTask("Reload failed", ex); - } - - return task; - }, - mBGExecutor); - }, - mBGExecutor); - } - - @DoNotStrip - private void setPausedInDebuggerMessage(@Nullable String message) { - if (message == null) { - mDevSupportManager.hidePausedInDebuggerOverlay(); - } else { - mDevSupportManager.showPausedInDebuggerOverlay( - message, - new PausedInDebuggerOverlayCommandListener() { - @Override - public void onResume() { - UiThreadUtil.assertOnUiThread(); - if (mReactHostInspectorTarget != null) { - mReactHostInspectorTarget.sendDebuggerResumeCommand(); - } - } - }); - } - } - - @DoNotStrip - private Map getHostMetadata() { - return AndroidInfoHelpers.getInspectorHostMetadata(mContext); - } - - @DoNotStrip - private void loadNetworkResource(String url, InspectorNetworkRequestListener listener) { - InspectorNetworkHelper.loadNetworkResource(url, listener); - } - - /** - * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until - * reload is finished, before destroying. - * - *

The destroy operation is asynchronous and the task returned by this method will complete - * when React Native gets destroyed. Note that the destroy operation will execute in multiple - * threads, in particular some of the sub-tasks will run in the UIThread. Calling {@link - * TaskInterface#waitForCompletion()} from the UIThread will lead into a deadlock. Use - * onDestroyFinished callback to be notified when React Native gets destroyed. - * - * @param reason describing why ReactHost is being destroyed (e.g. memory pressure) - * @param ex exception that caused the trigger to destroy ReactHost (or null) This exception will - * be used to log properly the cause of destroy operation. - * @param onDestroyFinished callback that will be called when React Native gets destroyed, the - * callback will run on a background thread. - * @return A task that completes when React Native gets destroyed. - */ - @Override - public TaskInterface destroy( - String reason, @Nullable Exception ex, Function1 onDestroyFinished) { - Task task = (Task) destroy(reason, ex); - return task.continueWith( - new Continuation() { - @Nullable - @Override - public Void then(Task task) throws Exception { - boolean instanceDestroyedSuccessfully = task.isCompleted() && !task.isFaulted(); - onDestroyFinished.invoke(instanceDestroyedSuccessfully); - return null; - } - }); - } - - /** - * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until - * reload is finished, before destroying. - * - *

The destroy operation is asynchronous and the task returned by this method will complete - * when React Native gets destroyed. Note that the destroy operation will execute in multiple - * threads, in particular some of the sub-tasks will run in the UIThread. Calling {@link - * TaskInterface#waitForCompletion()} from the UIThread will lead into a deadlock. - * - * @param reason {@link String} describing why ReactHost is being destroyed (e.g. memory pressure) - * @param ex {@link Exception} exception that caused the trigger to destroy ReactHost (or null) - * This exception will be used to log properly the cause of destroy operation. - * @return A task that completes when React Native gets destroyed. - */ - @Override - public TaskInterface destroy(String reason, @Nullable Exception ex) { - final String method = "destroy()"; - return Task.call( - () -> { - if (mReloadTask != null) { - log( - method, - "Reloading React Native. Waiting for reload to finish before destroying React" - + " Native."); - return mReloadTask.continueWithTask( - task -> getOrCreateDestroyTask(reason, ex), mBGExecutor); - } - return getOrCreateDestroyTask(reason, ex); - }, - mBGExecutor); - } - - private MemoryPressureListener createMemoryPressureListener(ReactInstance reactInstance) { - WeakReference weakReactInstance = new WeakReference<>(reactInstance); - return (level) -> { - mBGExecutor.execute( - () -> { - @Nullable ReactInstance strongReactInstance = weakReactInstance.get(); - if (strongReactInstance != null) { - strongReactInstance.handleMemoryPressure(level); - } - }); - }; - } - - @Nullable - /* package */ Activity getCurrentActivity() { - return mActivity.get(); - } - - @Nullable - /* package */ Activity getLastUsedActivity() { - @Nullable WeakReference lastUsedActivityWeakRef = mLastUsedActivity.get(); - if (lastUsedActivityWeakRef != null) { - return lastUsedActivityWeakRef.get(); - } - return null; - } - - private void setCurrentActivity(@Nullable Activity activity) { - mActivity.set(activity); - if (activity != null) { - mLastUsedActivity.set(new WeakReference<>(activity)); - } - } - - /** - * 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}. - */ - /* package */ EventDispatcher getEventDispatcher() { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance == null) { - return BlackHoleEventDispatcher.INSTANCE; - } - - return reactInstance.getEventDispatcher(); - } - - /* package */ - @Nullable - FabricUIManager getUIManager() { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance == null) { - return null; - } - return reactInstance.getFabricUIManager(); - } - - /* package */ boolean hasNativeModule(Class nativeModuleInterface) { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.hasNativeModule(nativeModuleInterface); - } - return false; - } - - /* package */ Collection getNativeModules() { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getNativeModules(); - } - return new ArrayList<>(); - } - - /* package */ - @Nullable - T getNativeModule(Class nativeModuleInterface) { - if (!ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE - && nativeModuleInterface == UIManagerModule.class) { - - ReactSoftExceptionLogger.logSoftExceptionVerbose( - TAG, - new ReactNoCrashBridgeNotAllowedSoftException( - "getNativeModule(UIManagerModule.class) cannot be called when the bridge is" - + " disabled")); - } - - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getNativeModule(nativeModuleInterface); - } - return null; - } - - /* package */ - @Nullable - NativeModule getNativeModule(String nativeModuleName) { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getNativeModule(nativeModuleName); - } - return null; - } - - /* package */ - @Nullable - RuntimeExecutor getRuntimeExecutor() { - final String method = "getRuntimeExecutor()"; - - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getBufferedRuntimeExecutor(); - } - raiseSoftException(method, "Tried to get runtime executor while instance is not ready"); - return null; - } - - /* package */ - @Nullable - CallInvokerHolder getJSCallInvokerHolder() { - final String method = "getJSCallInvokerHolder()"; - - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getJSCallInvokerHolder(); - } - raiseSoftException(method, "Tried to get JSCallInvokerHolder while instance is not ready"); - return null; - } - - /** - * To be called when the host activity receives an activity result. - * - * @param activity The host activity - */ - @ThreadConfined(UI) - @Override - public void onActivityResult( - Activity activity, int requestCode, int resultCode, @Nullable Intent data) { - final String method = - "onActivityResult(activity = \"" - + activity - + "\", requestCode = \"" - + requestCode - + "\", resultCode = \"" - + resultCode - + "\", data = \"" - + data - + "\")"; - - ReactContext currentContext = getCurrentReactContext(); - if (currentContext != null) { - currentContext.onActivityResult(activity, requestCode, resultCode, data); - } else { - raiseSoftException(method, "Tried to access onActivityResult while context is not ready"); - } - } - - /* To be called when focus has changed for the hosting window. */ - @ThreadConfined(UI) - @Override - public void onWindowFocusChange(boolean hasFocus) { - final String method = "onWindowFocusChange(hasFocus = \"" + hasFocus + "\")"; - - ReactContext currentContext = getCurrentReactContext(); - if (currentContext != null) { - currentContext.onWindowFocusChange(hasFocus); - } else { - raiseSoftException(method, "Tried to access onWindowFocusChange while context is not ready"); - } - } - - /* This method will give JS the opportunity to receive intents via Linking. - * - * @param intent The incoming intent - */ - @ThreadConfined(UI) - @Override - public void onNewIntent(Intent intent) { - final String method = "onNewIntent(intent = \"" + intent + "\")"; - - ReactContext currentContext = getCurrentReactContext(); - if (currentContext != null) { - String action = intent.getAction(); - Uri uri = intent.getData(); - - if (uri != null - && (Intent.ACTION_VIEW.equals(action) - || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action))) { - DeviceEventManagerModule deviceEventManagerModule = - currentContext.getNativeModule(DeviceEventManagerModule.class); - if (deviceEventManagerModule != null) { - deviceEventManagerModule.emitNewIntentReceived(uri); - } - } - currentContext.onNewIntent(getCurrentActivity(), intent); - } else { - raiseSoftException(method, "Tried to access onNewIntent while context is not ready"); - } - } - - @ThreadConfined(UI) - @Override - public void onConfigurationChanged(Context updatedContext) { - ReactContext currentReactContext = getCurrentReactContext(); - if (currentReactContext != null) { - if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) { - DisplayMetricsHolder.initDisplayMetrics(currentReactContext); - } - - AppearanceModule appearanceModule = - currentReactContext.getNativeModule(AppearanceModule.class); - - if (appearanceModule != null) { - appearanceModule.onConfigurationChanged(updatedContext); - } - } - } - - @Nullable - JavaScriptContextHolder getJavaScriptContextHolder() { - final ReactInstance reactInstance = mReactInstance; - if (reactInstance != null) { - return reactInstance.getJavaScriptContextHolder(); - } - return null; - } - - /* package */ - DefaultHardwareBackBtnHandler getDefaultBackButtonHandler() { - return () -> { - UiThreadUtil.assertOnUiThread(); - if (mDefaultHardwareBackBtnHandler != null) { - mDefaultHardwareBackBtnHandler.invokeDefaultOnBackPressed(); - } - }; - } - - /* package */ Task loadBundle(final JSBundleLoader bundleLoader) { - final String method = "loadBundle()"; - log(method, "Schedule"); - - return callWithExistingReactInstance( - method, - reactInstance -> { - log(method, "Execute"); - reactInstance.loadJSBundle(bundleLoader); - }, - null); - } - - /* 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(); - }, - null); - } - - /* package */ void handleHostException(Exception e) { - final String method = "handleHostException(message = \"" + e.getMessage() + "\")"; - log(method); - - if (mUseDevSupport) { - mDevSupportManager.handleException(e); - } else { - mReactHostDelegate.handleInstanceException(e); - } - destroy(method, 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); - }, - null); - } - - /* package */ void attachSurface(ReactSurfaceImpl surface) { - final String method = "attachSurface(surfaceId = " + surface.getSurfaceID() + ")"; - log(method); - - synchronized (mAttachedSurfaces) { - mAttachedSurfaces.add(surface); - } - } - - /* package */ void detachSurface(ReactSurfaceImpl surface) { - final String method = "detachSurface(surfaceId = " + surface.getSurfaceID() + ")"; - log(method); - - synchronized (mAttachedSurfaces) { - mAttachedSurfaces.remove(surface); - } - } - - /* package */ boolean isSurfaceAttached(ReactSurfaceImpl surface) { - synchronized (mAttachedSurfaces) { - return mAttachedSurfaces.contains(surface); - } - } - - /* package */ boolean isSurfaceWithModuleNameAttached(String moduleName) { - synchronized (mAttachedSurfaces) { - for (ReactSurfaceImpl surface : mAttachedSurfaces) { - if (surface.getModuleName().equals(moduleName)) { - return true; - } - } - return false; - } - } - - @Override - public void addBeforeDestroyListener(Function0 onBeforeDestroy) { - mBeforeDestroyListeners.add(onBeforeDestroy); - } - - @Override - public void removeBeforeDestroyListener(Function0 onBeforeDestroy) { - mBeforeDestroyListeners.remove(onBeforeDestroy); - } - - private interface ReactInstanceCalback { - void then(ReactInstance reactInstance); - } - - @ThreadConfined("ReactHost") - private @Nullable Task mStartTask = null; - - @ThreadConfined("ReactHost") - private Task getOrCreateStartTask() { - final String method = "getOrCreateStartTask()"; - if (mStartTask == null) { - log(method, "Schedule"); - if (ReactBuildConfig.DEBUG) { - Assertions.assertCondition( - ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture(), - "enableBridgelessArchitecture FeatureFlag must be set to start ReactNative."); - - Assertions.assertCondition( - ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer(), - "enableFabricRenderer FeatureFlag must be set to start ReactNative."); - - Assertions.assertCondition( - ReactNativeNewArchitectureFeatureFlags.useTurboModules(), - "useTurboModules FeatureFlag must be set to start ReactNative."); - } - if (ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE) { - Assertions.assertCondition( - !ReactNativeNewArchitectureFeatureFlags.useFabricInterop(), - "useFabricInterop FeatureFlag must be false when" - + " UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE == true."); - Assertions.assertCondition( - !ReactNativeNewArchitectureFeatureFlags.useTurboModuleInterop(), - "useTurboModuleInterop FeatureFlag must be false when" - + " UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE == true."); - } - mStartTask = - waitThenCallGetOrCreateReactInstanceTask() - .continueWithTask( - (task) -> { - if (task.isFaulted()) { - Exception ex = Assertions.assertNotNull(task.getError()); - if (mUseDevSupport) { - mDevSupportManager.handleException(ex); - } else { - mReactHostDelegate.handleInstanceException(ex); - } - // Wait for destroy to finish - return getOrCreateDestroyTask( - "getOrCreateStartTask() failure: " + ex.getMessage(), ex) - .continueWithTask(destroyTask -> Task.forError(ex)) - .makeVoid(); - } - return task.makeVoid(); - }, - mBGExecutor); - } - return mStartTask; - } - - @ThreadConfined(UI) - private void moveToHostDestroy(@Nullable ReactContext currentContext) { - mReactLifecycleStateManager.moveToOnHostDestroy(currentContext); - setCurrentActivity(null); - } - - private void raiseSoftException(String callingMethod, String message) { - raiseSoftException(callingMethod, message, null); - } - - private void raiseSoftException( - String callingMethod, String message, @Nullable Throwable throwable) { - final String method = "raiseSoftException(" + callingMethod + ")"; - log(method, message); - ReactSoftExceptionLogger.logSoftException( - TAG, new ReactNoCrashSoftException(method + ": " + message, throwable)); - } - - /** Schedule work on a ReactInstance that is already created. */ - private Task callWithExistingReactInstance( - final String callingMethod, - final ReactInstanceCalback continuation, - @Nullable Executor executor) { - final String method = "callWithExistingReactInstance(" + callingMethod + ")"; - - if (executor == null) { - executor = Task.IMMEDIATE_EXECUTOR; - } - - return mCreateReactInstanceTaskRef - .get() - .onSuccess( - task -> { - final ReactInstance reactInstance = task.getResult(); - if (reactInstance == null) { - raiseSoftException(method, "Execute: reactInstance is null. Dropping work."); - return FALSE; - } - - continuation.then(reactInstance); - return TRUE; - }, - executor); - } - - /** Create a ReactInstance if it doesn't exist already, and schedule work on it. */ - private Task callAfterGetOrCreateReactInstance( - final String callingMethod, - final ReactInstanceCalback runnable, - @Nullable Executor executor) { - final String method = "callAfterGetOrCreateReactInstance(" + callingMethod + ")"; - - if (executor == null) { - executor = Task.IMMEDIATE_EXECUTOR; - } - - return getOrCreateReactInstance() - .onSuccess( - task -> { - final ReactInstance reactInstance = task.getResult(); - if (reactInstance == null) { - raiseSoftException(method, "Execute: reactInstance is null. Dropping work."); - return null; - } - - runnable.then(reactInstance); - return null; - }, - executor) - .continueWith( - task -> { - if (task.isFaulted()) { - handleHostException(Assertions.assertNotNull(task.getError())); - } - return null; - }); - } - - private BridgelessReactContext getOrCreateReactContext() { - final String method = "getOrCreateReactContext()"; - return mBridgelessReactContextRef.getOrCreate( - () -> { - log(method, "Creating BridgelessReactContext"); - return new BridgelessReactContext(mContext, ReactHostImpl.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. - */ - private Task getOrCreateReactInstance() { - return Task.call(this::waitThenCallGetOrCreateReactInstanceTask, mBGExecutor); - } - - @ThreadConfined("ReactHost") - private Task waitThenCallGetOrCreateReactInstanceTask() { - return waitThenCallGetOrCreateReactInstanceTaskWithRetries(0, 4); - } - - @ThreadConfined("ReactHost") - private Task waitThenCallGetOrCreateReactInstanceTaskWithRetries( - int tryNum, int maxTries) { - final String method = "waitThenCallGetOrCreateReactInstanceTaskWithRetries"; - 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) -> waitThenCallGetOrCreateReactInstanceTaskWithRetries(tryNum + 1, maxTries), - mBGExecutor); - } - - raiseSoftException( - method, - "React Native is tearing down. Not wait for teardown to finish: reached max retries."); - } - - return getOrCreateReactInstanceTask(); - } - - private static class CreationResult { - final ReactInstance mInstance; - final ReactContext mContext; - final boolean mIsReloading; - - private CreationResult(ReactInstance instance, ReactContext context, boolean isReloading) { - mInstance = instance; - mContext = context; - mIsReloading = isReloading; - } - } - - @ThreadConfined("ReactHost") - private Task getOrCreateReactInstanceTask() { - final String method = "getOrCreateReactInstanceTask()"; - log(method); - - return mCreateReactInstanceTaskRef.getOrCreate( - () -> { - log(method, "Start"); - Assertions.assertCondition( - !mHostInvalidated, "Cannot start a new ReactInstance on an invalidated ReactHost"); - - ReactMarker.logMarker( - ReactMarkerConstants.REACT_BRIDGELESS_LOADING_START, BRIDGELESS_MARKER_INSTANCE_KEY); - - Task creationTask = - getJsBundleLoader() - .onSuccess( - task -> { - final JSBundleLoader bundleLoader = - Assertions.assertNotNull(task.getResult()); - final BridgelessReactContext reactContext = getOrCreateReactContext(); - final DevSupportManager devSupportManager = getDevSupportManager(); - reactContext.setJSExceptionHandler(devSupportManager); - - log(method, "Creating ReactInstance"); - final ReactInstance instance = - new ReactInstance( - reactContext, - mReactHostDelegate, - mComponentFactory, - devSupportManager, - this::handleHostException, - mUseDevSupport, - getOrCreateReactHostInspectorTarget()); - mReactInstance = instance; - - MemoryPressureListener memoryPressureListener = - createMemoryPressureListener(instance); - mMemoryPressureListener = memoryPressureListener; - mMemoryPressureRouter.addMemoryPressureListener(memoryPressureListener); - - // Eagerly initialize turbo modules in parallel with JS bundle execution - // as TurboModuleManager will handle any concurrent access - instance.initializeEagerTurboModules(); - - 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 ensure that 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); - }); - - return new CreationResult(instance, reactContext, mReloadTask != null); - }, - mBGExecutor); - - Continuation lifecycleUpdateTask = - task -> { - CreationResult result = Assertions.assertNotNull(task.getResult()); - final ReactInstance reactInstance = result.mInstance; - final ReactContext reactContext = result.mContext; - final boolean isReloading = result.mIsReloading; - final boolean isManagerResumed = - mReactLifecycleStateManager.getLifecycleState() == LifecycleState.RESUMED; - - /** - * ReactContext.onHostResume() should only be called when the user navigates to the - * first React Native screen. - * - *

During init: The application puts the React manager in a resumed state, when - * the user navigates to a React Native screen. Two types of init: (1) If React - * Native init happens when the user navigates to a React Native screen, the React - * manager will get resumed on init start, so ReactContext.onHostResume() will be - * executed here. (2) If React Native init happens before the user navigates to a - * React Native screen (i.e: React Native is preloaded), the React manager won't be - * in a resumed state here. So ReactContext.onHostResume() won't be executed here. - * But, when the user navigates to their first React Native screen, the application - * will call ReactHost.onHostResume(). That will call ReactContext.onHostResume(). - * - *

During reloads, if the manager isn't resumed, call - * ReactContext.onHostResume(). If React Native is reloading, it seems reasonable to - * assume that: (1) We must have navigated to a React Native screen in the past, or - * (2) We must be on a React Native screen. - */ - if (isReloading && !isManagerResumed) { - mReactLifecycleStateManager.moveToOnHostResume( - reactContext, getCurrentActivity()); - } else { - /** - * Call ReactContext.onHostResume() only when already in the resumed state which - * aligns with the bridge https://fburl.com/diffusion/2qhxmudv. - */ - mReactLifecycleStateManager.resumeReactContextIfHostResumed( - reactContext, getCurrentActivity()); - } - - log(method, "Executing ReactInstanceEventListeners"); - for (ReactInstanceEventListener listener : mReactInstanceEventListeners) { - if (listener != null) { - listener.onReactContextInitialized(reactContext); - } - } - return reactInstance; - }; - - creationTask.onSuccess(lifecycleUpdateTask, mUIExecutor); - return creationTask.onSuccess( - task -> Assertions.assertNotNull(task.getResult()).mInstance, - Task.IMMEDIATE_EXECUTOR); - }); - } - - private Task getJsBundleLoader() { - final String method = "getJSBundleLoader()"; - log(method); - - if (mUseDevSupport && mAllowPackagerServerAccess) { - return isMetroRunning() - .onSuccessTask( - task -> { - boolean isMetroRunning = Assertions.assertNotNull(task.getResult()); - if (isMetroRunning) { - // Since metro is running, fetch the JS bundle from the server - return loadJSBundleFromMetro(); - } - return Task.forResult(mReactHostDelegate.getJsBundleLoader()); - }, - mBGExecutor); - } else { - if (ReactBuildConfig.DEBUG) { - 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. - */ - try { - return Task.forResult(mReactHostDelegate.getJsBundleLoader()); - } catch (Exception e) { - return Task.forError(e); - } - } - } - - 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(); - } - - private Task loadJSBundleFromMetro() { - final String method = "loadJSBundleFromMetro()"; - log(method); - - final TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - final DevSupportManagerBase asyncDevSupportManager = - ((DevSupportManagerBase) getDevSupportManager()); - String bundleURL = - asyncDevSupportManager - .getDevServerHelper() - .getDevServerBundleURL( - Assertions.assertNotNull(asyncDevSupportManager.getJSAppBundleName())); - - asyncDevSupportManager.reloadJSFromServer( - bundleURL, - new BundleLoadCallback() { - @Override - public void onSuccess() { - log(method, "Creating BundleLoader"); - JSBundleLoader bundleLoader = - JSBundleLoader.createCachedBundleFromNetworkLoader( - bundleURL, asyncDevSupportManager.getDownloadedJSBundleFile()); - taskCompletionSource.setResult(bundleLoader); - } - - @Override - public void onError(Exception cause) { - taskCompletionSource.setError(cause); - } - }); - - 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); - } - - private void stopAttachedSurfaces(String method, ReactInstance reactInstance) { - log(method, "Stopping all React Native surfaces"); - synchronized (mAttachedSurfaces) { - for (ReactSurfaceImpl surface : mAttachedSurfaces) { - reactInstance.stopSurface(surface); - surface.clear(); - } - } - } - - private void startAttachedSurfaces(String method, ReactInstance reactInstance) { - log(method, "Restarting previously running React Native Surfaces"); - synchronized (mAttachedSurfaces) { - for (ReactSurfaceImpl surface : mAttachedSurfaces) { - reactInstance.startSurface(surface); - } - } - } - - @ThreadConfined("ReactHost") - private @Nullable Task mReloadTask = null; - - private interface ReactInstanceTaskUnwrapper { - @Nullable - ReactInstance unwrap(Task task, String stage); - } - - private ReactInstanceTaskUnwrapper createReactInstanceUnwrapper( - String tag, String method, String reason) { - - return (task, stage) -> { - final ReactInstance reactInstance = task.getResult(); - final ReactInstance currentReactInstance = mReactInstance; - - final String stageLabel = "Stage: " + stage; - final String reasonLabel = tag + " reason: " + reason; - if (task.isFaulted()) { - final Exception ex = Assertions.assertNotNull(task.getError()); - final String faultLabel = "Fault reason: " + ex.getMessage(); - raiseSoftException( - method, - tag - + ": ReactInstance task faulted. " - + stageLabel - + ". " - + faultLabel - + ". " - + reasonLabel); - return currentReactInstance; - } - - if (task.isCancelled()) { - raiseSoftException( - method, tag + ": ReactInstance task cancelled. " + stageLabel + ". " + reasonLabel); - return currentReactInstance; - } - - if (reactInstance == null) { - raiseSoftException( - method, tag + ": ReactInstance task returned null. " + stageLabel + ". " + reasonLabel); - return currentReactInstance; - } - - if (currentReactInstance != null && reactInstance != currentReactInstance) { - raiseSoftException( - method, - tag - + ": Detected two different ReactInstances. Returning old. " - + stageLabel - + ". " - + reasonLabel); - } - - return reactInstance; - }; - } - - /** - * 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 getOrCreateReloadTask(String reason) { - final String method = "getOrCreateReloadTask()"; - log(method); - - // Log how React Native is destroyed - // TODO(T136397487): Remove after Venice is shipped to 100% - raiseSoftException(method, reason); - - ReactInstanceTaskUnwrapper reactInstanceTaskUnwrapper = - createReactInstanceUnwrapper("Reload", method, reason); - - if (mReloadTask == null) { - // When using the immediate executor, we want to avoid scheduling any further work immediately - // when destruction is kicked off. - log(method, "Resetting createReactInstance task ref"); - mReloadTask = - mCreateReactInstanceTaskRef - .getAndReset() - .continueWithTask( - (task) -> { - log(method, "Starting React Native reload"); - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "1: Starting reload"); - - unregisterInstanceFromInspector(reactInstance); - - 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.forResult(reactInstance); - }, - mUIExecutor) - .continueWithTask( - task -> { - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "2: Surface shutdown"); - - if (reactInstance == null) { - raiseSoftException(method, "Skipping surface shutdown: ReactInstance null"); - return task; - } - - stopAttachedSurfaces(method, reactInstance); - return task; - }, - mBGExecutor) - .continueWithTask( - (task) -> { - reactInstanceTaskUnwrapper.unwrap(task, "3: Destroying ReactContext"); - for (Function0 destroyListener : mBeforeDestroyListeners) { - destroyListener.invoke(); - } - - if (mMemoryPressureListener != null) { - log(method, "Removing memory pressure listener"); - mMemoryPressureRouter.removeMemoryPressureListener(mMemoryPressureListener); - } - - final ReactContext reactContext = mBridgelessReactContextRef.getNullable(); - if (reactContext != null) { - log(method, "Resetting ReactContext ref"); - mBridgelessReactContextRef.reset(); - - 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 = - reactInstanceTaskUnwrapper.unwrap(task, "4: Destroying ReactInstance"); - - if (reactInstance == null) { - raiseSoftException( - method, "Skipping ReactInstance.destroy(): ReactInstance null"); - } else { - log(method, "Resetting ReactInstance ptr"); - mReactInstance = null; - - log(method, "Destroying ReactInstance"); - reactInstance.destroy(); - } - - log(method, "Resetting start task ref"); - mStartTask = null; - - // Kickstart a new ReactInstance create - return getOrCreateReactInstanceTask(); - }, - mBGExecutor) - .continueWithTask( - task -> { - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "5: Restarting surfaces"); - - if (reactInstance == null) { - raiseSoftException(method, "Skipping surface restart: ReactInstance null"); - return task; - } - - startAttachedSurfaces(method, reactInstance); - - return task; - }, - mBGExecutor) - .continueWithTask( - task -> { - if (task.isFaulted()) { - Exception fault = Assertions.assertNotNull(task.getError()); - raiseSoftException( - method, - "Error during reload. ReactInstance task faulted. Fault reason: " - + fault.getMessage() - + ". Reload reason: " - + reason, - task.getError()); - } - - if (task.isCancelled()) { - raiseSoftException( - method, - "Error during reload. ReactInstance task cancelled. Reload reason: " - + reason); - } - - log(method, "Resetting reload task ref"); - mReloadTask = null; - return task; - }, - mBGExecutor); - } - - return mReloadTask; - } - - @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 getOrCreateDestroyTask(final String reason, @Nullable Exception ex) { - final String method = "getOrCreateDestroyTask()"; - log(method); - - // Log how React Native is destroyed - // TODO(T136397487): Remove after Venice is shipped to 100% - raiseSoftException(method, reason, ex); - - ReactInstanceTaskUnwrapper reactInstanceTaskUnwrapper = - createReactInstanceUnwrapper("Destroy", method, reason); - - if (mDestroyTask == null) { - // When using the immediate executor, we want to avoid scheduling any further work immediately - // when destruction is kicked off. - log(method, "Resetting createReactInstance task ref"); - mDestroyTask = - mCreateReactInstanceTaskRef - .getAndReset() - .continueWithTask( - task -> { - log(method, "Starting React Native destruction"); - - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "1: Starting destroy"); - - unregisterInstanceFromInspector(reactInstance); - - if (mHostInvalidated) { - // If the host has been invalidated, now that the current context/instance - // has been unregistered, we can safely destroy the host's inspector - // target. - if (mReactHostInspectorTarget != null) { - mReactHostInspectorTarget.close(); - mReactHostInspectorTarget = null; - } - } - - // 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); - - return Task.forResult(reactInstance); - }, - mUIExecutor) - .continueWithTask( - task -> { - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "2: Stopping surfaces"); - if (reactInstance == null) { - raiseSoftException(method, "Skipping surface shutdown: ReactInstance null"); - return task; - } - - // Step 3: Stop all React Native surfaces - stopAttachedSurfaces(method, reactInstance); - synchronized (mAttachedSurfaces) { - mAttachedSurfaces.clear(); - } - - return task; - }, - mBGExecutor) - .continueWithTask( - task -> { - reactInstanceTaskUnwrapper.unwrap(task, "3: Destroying ReactContext"); - for (Function0 destroyListener : mBeforeDestroyListeners) { - destroyListener.invoke(); - } - - final ReactContext reactContext = mBridgelessReactContextRef.getNullable(); - if (reactContext == null) { - raiseSoftException(method, "ReactContext is null. Destroy reason: " + reason); - } - - // Step 4: De-register the memory pressure listener - log(method, "Destroying MemoryPressureRouter"); - mMemoryPressureRouter.destroy(mContext); - - if (reactContext != null) { - log(method, "Resetting ReactContext ref"); - mBridgelessReactContextRef.reset(); - - log(method, "Destroying ReactContext"); - reactContext.destroy(); - } - - // Reset current activity - setCurrentActivity(null); - - // Clear ResourceIdleDrawableIdMap - ResourceDrawableIdHelper.getInstance().clear(); - - return task; - }, - mUIExecutor) - .continueWithTask( - task -> { - final ReactInstance reactInstance = - reactInstanceTaskUnwrapper.unwrap(task, "4: Destroying ReactInstance"); - - if (reactInstance == null) { - raiseSoftException( - method, "Skipping ReactInstance.destroy(): ReactInstance null"); - } else { - log(method, "Resetting ReactInstance ptr"); - mReactInstance = null; - - log(method, "Destroying ReactInstance"); - reactInstance.destroy(); - } - - log(method, "Resetting start task ref"); - mStartTask = null; - - log(method, "Resetting destroy task ref"); - mDestroyTask = null; - return task; - }, - mBGExecutor) - .continueWith( - task -> { - if (task.isFaulted()) { - Exception fault = Assertions.assertNotNull(task.getError()); - raiseSoftException( - method, - "React destruction failed. ReactInstance task faulted. Fault reason: " - + fault.getMessage() - + ". Destroy reason: " - + reason, - task.getError()); - } - - if (task.isCancelled()) { - raiseSoftException( - method, - "React destruction failed. ReactInstance task cancelled. Destroy reason: " - + reason); - } - return null; - }); - } - - return mDestroyTask; - } - - @VisibleForTesting - /* package */ @Nullable - ReactHostInspectorTarget getOrCreateReactHostInspectorTarget() { - if (mReactHostInspectorTarget == null && InspectorFlags.getFuseboxEnabled()) { - // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. - mReactHostInspectorTarget = new ReactHostInspectorTarget(this); - } - - return mReactHostInspectorTarget; - } - - @ThreadConfined(UI) - @VisibleForTesting - /* package */ void unregisterInstanceFromInspector(final @Nullable ReactInstance reactInstance) { - if (reactInstance != null) { - if (InspectorFlags.getFuseboxEnabled()) { - Assertions.assertCondition( - mReactHostInspectorTarget != null && mReactHostInspectorTarget.isValid(), - "Host inspector target destroyed before instance was unregistered"); - } - reactInstance.unregisterFromInspector(); - } - } - - @Override - public void invalidate() { - FLog.d(TAG, "ReactHostImpl.invalidate()"); - mHostInvalidated = true; - destroy("ReactHostImpl.invalidate()", null); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt new file mode 100644 index 00000000000..536b0542f78 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -0,0 +1,1471 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.runtime + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.nfc.NfcAdapter +import android.os.Bundle +import com.facebook.common.logging.FLog +import com.facebook.infer.annotation.Assertions +import com.facebook.infer.annotation.ThreadConfined +import com.facebook.infer.annotation.ThreadSafe +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.MemoryPressureRouter +import com.facebook.react.ReactHost +import com.facebook.react.ReactInstanceEventListener +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.JSBundleLoader +import com.facebook.react.bridge.JavaScriptContextHolder +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.RuntimeExecutor +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.bridge.queue.ReactQueueConfiguration +import com.facebook.react.common.LifecycleState +import com.facebook.react.common.annotations.FrameworkAPI +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.build.ReactBuildConfig +import com.facebook.react.devsupport.DefaultDevSupportManagerFactory +import com.facebook.react.devsupport.DevSupportManagerBase +import com.facebook.react.devsupport.DevSupportManagerFactory +import com.facebook.react.devsupport.InspectorFlags +import com.facebook.react.devsupport.inspector.InspectorNetworkHelper +import com.facebook.react.devsupport.inspector.InspectorNetworkRequestListener +import com.facebook.react.devsupport.interfaces.BundleLoadCallback +import com.facebook.react.devsupport.interfaces.DevSupportManager +import com.facebook.react.devsupport.interfaces.DevSupportManager.PausedInDebuggerOverlayCommandListener +import com.facebook.react.fabric.ComponentFactory +import com.facebook.react.fabric.FabricUIManager +import com.facebook.react.interfaces.TaskInterface +import com.facebook.react.interfaces.fabric.ReactSurface +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags +import com.facebook.react.modules.appearance.AppearanceModule +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.facebook.react.modules.systeminfo.AndroidInfoHelpers +import com.facebook.react.runtime.internal.bolts.Task +import com.facebook.react.runtime.internal.bolts.TaskCompletionSource +import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder +import com.facebook.react.uimanager.DisplayMetricsHolder +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.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import kotlin.Unit +import kotlin.concurrent.Volatile + +/** + * A ReactHost is an object that manages a single [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](https://github.com/BoltsFramework/Bolts-Android.tasks) + */ +@DoNotStrip +@ThreadSafe +@OptIn(UnstableReactNativeAPI::class, FrameworkAPI::class) +public class ReactHostImpl( + private val context: Context, + private val reactHostDelegate: ReactHostDelegate, + private val componentFactory: ComponentFactory, + private val bgExecutor: Executor = Executors.newSingleThreadExecutor(), + private val uiExecutor: Executor = Task.UI_THREAD_EXECUTOR, + private val allowPackagerServerAccess: Boolean, + private val useDevSupport: Boolean, + devSupportManagerFactory: DevSupportManagerFactory? = null, +) : ReactHost { + public override val devSupportManager: DevSupportManager = + (devSupportManagerFactory ?: DefaultDevSupportManagerFactory()).create( + applicationContext = context.applicationContext, + reactInstanceManagerHelper = ReactHostImplDevHelper(this), + packagerPathForJSBundleName = reactHostDelegate.jsMainModulePath, + enableOnCreate = true, + redBoxHandler = null, + devBundleDownloadListener = null, + minNumShakes = 2, + customPackagerCommandHandlers = null, + surfaceDelegateFactory = null, + devLoadingViewManager = null, + pausedInDebuggerOverlayManager = null, + useDevSupport = useDevSupport) + public override val memoryPressureRouter: MemoryPressureRouter = MemoryPressureRouter(context) + + private val attachedSurfaces: MutableSet = HashSet() + + // todo: T192399917 This no longer needs to store the react instance + private val createReactInstanceTaskRef = BridgelessAtomicRef(Task.forResult(null)) + private var reactInstance: ReactInstance? = null + + private val bridgelessReactContextRef = BridgelessAtomicRef() + + private val activity = AtomicReference() + private val lastUsedActivityRef = AtomicReference(WeakReference(null)) + private val bridgelessReactStateTracker = BridgelessReactStateTracker(ReactBuildConfig.DEBUG) + private val reactLifecycleStateManager = ReactLifecycleStateManager(bridgelessReactStateTracker) + private val id = counter.getAndIncrement() + private var memoryPressureListener: MemoryPressureListener? = null + private var defaultHardwareBackBtnHandler: DefaultHardwareBackBtnHandler? = null + + private val reactInstanceEventListeners: MutableList = + CopyOnWriteArrayList() + private val beforeDestroyListeners: MutableList<() -> Unit> = CopyOnWriteArrayList() + + private var reactHostInspectorTarget: ReactHostInspectorTarget? = null + + @Volatile private var hostInvalidated = false + + public constructor( + context: Context, + delegate: ReactHostDelegate, + componentFactory: ComponentFactory, + allowPackagerServerAccess: Boolean, + useDevSupport: Boolean + ) : this( + context, + delegate, + componentFactory, + Executors.newSingleThreadExecutor(), + Task.UI_THREAD_EXECUTOR, + allowPackagerServerAccess, + useDevSupport) + + public override val lifecycleState: LifecycleState + get() = reactLifecycleStateManager.lifecycleState + + /** + * 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. + */ + override fun start(): TaskInterface = Task.call({ getOrCreateStartTask() }, bgExecutor) + + /** Initialize and run a React Native surface in a background without mounting real views. */ + internal fun prerenderSurface(surface: ReactSurfaceImpl): TaskInterface { + val method = "prerenderSurface(surfaceId = ${surface.surfaceID})" + log(method, "Schedule") + + attachSurface(surface) + return callAfterGetOrCreateReactInstance(method, bgExecutor) { reactInstance: 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. + */ + internal fun startSurface(surface: ReactSurfaceImpl): TaskInterface { + val method = "startSurface(surfaceId = ${surface.surfaceID})" + log(method, "Schedule") + + attachSurface(surface) + return callAfterGetOrCreateReactInstance(method, bgExecutor) { reactInstance: 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. + */ + internal fun stopSurface(surface: ReactSurfaceImpl): TaskInterface { + val method = "stopSurface(surfaceId = ${surface.surfaceID})" + log(method, "Schedule") + + detachSurface(surface) + return callWithExistingReactInstance(method, bgExecutor) { reactInstance: ReactInstance -> + log(method, "Execute") + reactInstance.stopSurface(surface) + } + .makeVoid() + } + + /** + * To be called when the host activity is resumed. + * + * @param activity The host activity + */ + @ThreadConfined(ThreadConfined.UI) + override fun onHostResume( + activity: Activity?, + defaultBackButtonImpl: DefaultHardwareBackBtnHandler? + ) { + defaultHardwareBackBtnHandler = defaultBackButtonImpl + onHostResume(activity) + } + + @ThreadConfined(ThreadConfined.UI) + override fun onHostResume(activity: Activity?) { + val method = "onHostResume(activity)" + log(method) + + currentActivity = activity + + maybeEnableDevSupport(true) + reactLifecycleStateManager.moveToOnHostResume(currentReactContext, activity) + } + + @ThreadConfined(ThreadConfined.UI) + override fun onHostLeaveHint(activity: Activity?) { + val method = "onUserLeaveHint(activity)" + log(method) + + currentReactContext?.onUserLeaveHint(activity) + } + + @ThreadConfined(ThreadConfined.UI) + override fun onHostPause(activity: Activity?) { + val method = "onHostPause(activity)" + log(method) + + val currentActivity = this.currentActivity + if (currentActivity != null) { + val currentActivityClass = currentActivity.javaClass.simpleName + val activityClass = if (activity == null) "null" else activity.javaClass.simpleName + Assertions.assertCondition( + activity === currentActivity, + "Pausing an activity that is not the current activity, this is incorrect! Current activity: $currentActivityClass Paused activity: $activityClass") + } + + maybeEnableDevSupport(false) + defaultHardwareBackBtnHandler = null + reactLifecycleStateManager.moveToOnHostPause(currentReactContext, currentActivity) + } + + /** To be called when the host activity is paused. */ + @ThreadConfined(ThreadConfined.UI) + override fun onHostPause() { + val method = "onHostPause()" + log(method) + + maybeEnableDevSupport(false) + defaultHardwareBackBtnHandler = null + reactLifecycleStateManager.moveToOnHostPause(currentReactContext, currentActivity) + } + + /** To be called when the host activity is destroyed. */ + @ThreadConfined(ThreadConfined.UI) + override fun onHostDestroy() { + val method = "onHostDestroy()" + log(method) + + maybeEnableDevSupport(false) + moveToHostDestroy(currentReactContext) + } + + @ThreadConfined(ThreadConfined.UI) + override fun onHostDestroy(activity: Activity?) { + val method = "onHostDestroy(activity)" + log(method) + + val currentActivity = this.currentActivity + + if (currentActivity === activity) { + maybeEnableDevSupport(false) + moveToHostDestroy(currentReactContext) + } + } + + private fun maybeEnableDevSupport(enabled: Boolean) { + if (useDevSupport) { + devSupportManager.devSupportEnabled = enabled + } + } + + public override val currentReactContext: ReactContext? + /** + * Returns current ReactContext which could be nullable if ReactInstance hasn't been created. + * + * @return The [BridgelessReactContext] associated with ReactInstance. + */ + get() = bridgelessReactContextRef.value + + override fun createSurface( + context: Context, + moduleName: String, + initialProps: Bundle? + ): ReactSurface { + val surface = ReactSurfaceImpl(context, moduleName, initialProps) + val surfaceView = ReactSurfaceView(context, surface) + surfaceView.setShouldLogContentAppeared(true) + surface.attachView(surfaceView) + surface.attach(this) + return surface + } + + internal val isInstanceInitialized: Boolean + get() = reactInstance != null + + @ThreadConfined(ThreadConfined.UI) + override fun onBackPressed(): Boolean { + UiThreadUtil.assertOnUiThread() + val reactInstance = reactInstance ?: return false + + val deviceEventManagerModule = + reactInstance.getNativeModule(DeviceEventManagerModule::class.java) ?: return false + + deviceEventManagerModule.emitHardwareBackPressed() + return true + } + + public override val reactQueueConfiguration: ReactQueueConfiguration? + get() = reactInstance?.reactQueueConfiguration + + /** Add a listener to be notified of ReactInstance events. */ + override fun addReactInstanceEventListener(listener: ReactInstanceEventListener) { + reactInstanceEventListeners.add(listener) + } + + /** Remove a listener previously added with [addReactInstanceEventListener]. */ + override fun removeReactInstanceEventListener(listener: ReactInstanceEventListener) { + reactInstanceEventListeners.remove(listener) + } + + /** + * Entrypoint to reload the ReactInstance. If the ReactInstance is destroying, will wait until + * destroy is finished, before reloading. + * + * @param reason [String] describing why ReactHost is being reloaded (e.g. js error, user tap on + * reload button) + * @return A task that completes when React Native reloads + */ + override fun reload(reason: String): TaskInterface = + Task.call( + { + val reloadTask = + (destroyTask?.let { destroyTask -> + log( + "reload()", + "Waiting for destroy to finish, before reloading React Native.") + destroyTask.continueWithTask({ getOrCreateReloadTask(reason) }, bgExecutor) + } ?: getOrCreateReloadTask(reason)) + .makeVoid() + reloadTask.continueWithTask( + { task -> + if (task.isFaulted()) { + val ex = checkNotNull(task.getError()) + if (useDevSupport) { + devSupportManager.handleException(ex) + } else { + reactHostDelegate.handleInstanceException(ex) + } + getOrCreateDestroyTask("Reload failed", ex) + } else { + task + } + }, + bgExecutor) + }, + bgExecutor) + + @DoNotStrip + private fun setPausedInDebuggerMessage(message: String?) { + if (message == null) { + devSupportManager.hidePausedInDebuggerOverlay() + } else { + devSupportManager.showPausedInDebuggerOverlay( + message, + object : PausedInDebuggerOverlayCommandListener { + override fun onResume() { + UiThreadUtil.assertOnUiThread() + reactHostInspectorTarget?.sendDebuggerResumeCommand() + } + }) + } + } + + @get:DoNotStrip + private val hostMetadata: Map + get() = AndroidInfoHelpers.getInspectorHostMetadata(context) + + @DoNotStrip + private fun loadNetworkResource(url: String, listener: InspectorNetworkRequestListener) { + InspectorNetworkHelper.loadNetworkResource(url, listener) + } + + /** + * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until + * reload is finished, before destroying. + * + * The destroy operation is asynchronous and the task returned by this method will complete when + * React Native gets destroyed. Note that the destroy operation will execute in multiple threads, + * in particular some of the sub-tasks will run in the UIThread. Calling + * [TaskInterface#waitForCompletion()] from the UIThread will lead into a deadlock. Use + * onDestroyFinished callback to be notified when React Native gets destroyed. + * + * @param reason describing why ReactHost is being destroyed (e.g. memory pressure) + * @param ex exception that caused the trigger to destroy ReactHost (or null) This exception will + * be used to log properly the cause of destroy operation. + * @param onDestroyFinished callback that will be called when React Native gets destroyed, the + * callback will run on a background thread. + * @return A task that completes when React Native gets destroyed. + */ + override fun destroy( + reason: String, + ex: Exception?, + onDestroyFinished: (instanceDestroyedSuccessfully: Boolean) -> Unit + ): TaskInterface { + val destroyTask = destroy(reason, ex) as Task + return destroyTask.continueWith({ task: Task -> + val instanceDestroyedSuccessfully = task.isCompleted() && !task.isFaulted() + onDestroyFinished(instanceDestroyedSuccessfully) + null + }) + } + + /** + * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until + * reload is finished, before destroying. + * + * The destroy operation is asynchronous and the task returned by this method will complete when + * React Native gets destroyed. Note that the destroy operation will execute in multiple threads, + * in particular some of the sub-tasks will run in the UIThread. Calling + * [TaskInterface#waitForCompletion()] from the UIThread will lead into a deadlock. + * + * @param reason [String] describing why ReactHost is being destroyed (e.g. memory pressure) + * @param ex [Exception] exception that caused the trigger to destroy ReactHost (or null). This + * exception will be used to log properly the cause of destroy operation. + * @return A task that completes when React Native gets destroyed. + */ + override fun destroy(reason: String, ex: Exception?): TaskInterface = + Task.call( + { + val reloadTask = reloadTask + if (reloadTask != null) { + log( + "destroy()", + "Reloading React Native. Waiting for reload to finish before destroying React Native.") + reloadTask.continueWithTask({ getOrCreateDestroyTask(reason, ex) }, bgExecutor) + } else { + getOrCreateDestroyTask(reason, ex) + } + }, + bgExecutor) + + private fun createMemoryPressureListener(reactInstance: ReactInstance): MemoryPressureListener { + val weakReactInstance = WeakReference(reactInstance) + return MemoryPressureListener { level: Int -> + bgExecutor.execute { + val strongReactInstance = weakReactInstance.get() + strongReactInstance?.handleMemoryPressure(level) + } + } + } + + internal var currentActivity: Activity? + get() = activity.get() + private set(activity) { + this.activity.set(activity) + if (activity != null) { + lastUsedActivityRef.set(WeakReference(activity)) + } + } + + internal val lastUsedActivity: Activity? + get() = lastUsedActivityRef.get()?.get() + + internal val eventDispatcher: EventDispatcher + /** + * Get the [EventDispatcher] from the [FabricUIManager]. This always returns an EventDispatcher, + * even if the instance isn't alive; in that case, it returns a [BlackHoleEventDispatcher] which + * no-ops. + * + * @return The real [EventDispatcher] if the instance is alive; otherwise, a + * [BlackHoleEventDispatcher]. + */ + get() = reactInstance?.eventDispatcher ?: BlackHoleEventDispatcher + + internal val uiManager: FabricUIManager? + get() = reactInstance?.fabricUIManager + + internal fun hasNativeModule(nativeModuleInterface: Class): Boolean = + reactInstance?.hasNativeModule(nativeModuleInterface) ?: false + + internal val nativeModules: Collection = reactInstance?.nativeModules ?: listOf() + + internal fun getNativeModule(nativeModuleInterface: Class): T? { + if (!ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE && + nativeModuleInterface == UIManagerModule::class.java) { + ReactSoftExceptionLogger.logSoftExceptionVerbose( + TAG, + ReactNoCrashBridgeNotAllowedSoftException( + "getNativeModule(UIManagerModule.class) cannot be called when the bridge is disabled")) + } + + return reactInstance?.getNativeModule(nativeModuleInterface) + } + + internal fun getNativeModule(nativeModuleName: String): NativeModule? = + reactInstance?.getNativeModule(nativeModuleName) + + internal val runtimeExecutor: RuntimeExecutor? + get() { + reactInstance?.apply { + return getBufferedRuntimeExecutor() + } + + raiseSoftException( + "getRuntimeExecutor()", "Tried to get runtime executor while instance is not ready") + return null + } + + internal val jsCallInvokerHolder: CallInvokerHolder? + get() { + reactInstance?.apply { + return getJSCallInvokerHolder() + } + + raiseSoftException( + "getJSCallInvokerHolder()", + "Tried to get JSCallInvokerHolder while instance is not ready") + return null + } + + /** + * To be called when the host activity receives an activity result. + * + * @param activity The host activity + */ + @ThreadConfined(ThreadConfined.UI) + override fun onActivityResult( + activity: Activity, + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + val method = + "onActivityResult(activity = \"$activity\", requestCode = \"$requestCode\", resultCode = \"$resultCode\", data = \"$data\")" + + val currentContext = currentReactContext + if (currentContext != null) { + currentContext.onActivityResult(activity, requestCode, resultCode, data) + } else { + raiseSoftException(method, "Tried to access onActivityResult while context is not ready") + } + } + + /* To be called when focus has changed for the hosting window. */ + @ThreadConfined(ThreadConfined.UI) + override fun onWindowFocusChange(hasFocus: Boolean) { + val currentContext = currentReactContext + if (currentContext != null) { + currentContext.onWindowFocusChange(hasFocus) + } else { + val method = "onWindowFocusChange(hasFocus = \"$hasFocus\")" + raiseSoftException(method, "Tried to access onWindowFocusChange while context is not ready") + } + } + + /** + * This method will give JS the opportunity to receive intents via Linking. + * + * @param intent The incoming intent + */ + @ThreadConfined(ThreadConfined.UI) + override fun onNewIntent(intent: Intent) { + val currentContext = currentReactContext + if (currentContext != null) { + val action = intent.action + val uri = intent.data + + if (uri != null && + (Intent.ACTION_VIEW == action || NfcAdapter.ACTION_NDEF_DISCOVERED == action)) { + val deviceEventManagerModule = + currentContext.getNativeModule(DeviceEventManagerModule::class.java) + deviceEventManagerModule?.emitNewIntentReceived(uri) + } + currentContext.onNewIntent(currentActivity, intent) + } else { + val method = "onNewIntent(intent = \"$intent\")" + raiseSoftException(method, "Tried to access onNewIntent while context is not ready") + } + } + + @ThreadConfined(ThreadConfined.UI) + override fun onConfigurationChanged(context: Context) { + val currentReactContext = this.currentReactContext + if (currentReactContext != null) { + if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) { + DisplayMetricsHolder.initDisplayMetrics(currentReactContext) + } + + val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java) + appearanceModule?.onConfigurationChanged(context) + } + } + + internal val javaScriptContextHolder: JavaScriptContextHolder? + get() = reactInstance?.javaScriptContextHolder + + internal val defaultBackButtonHandler: DefaultHardwareBackBtnHandler + get() = DefaultHardwareBackBtnHandler { + UiThreadUtil.assertOnUiThread() + defaultHardwareBackBtnHandler?.invokeDefaultOnBackPressed() + } + + internal fun loadBundle(bundleLoader: JSBundleLoader): Task { + val method = "loadBundle()" + log(method, "Schedule") + + return callWithExistingReactInstance(method) { reactInstance: ReactInstance -> + log(method, "Execute") + reactInstance.loadJSBundle(bundleLoader) + } + } + + internal fun registerSegment(segmentId: Int, path: String, callback: Callback?): Task { + val method = "registerSegment(segmentId = \"$segmentId\", path = \"$path\")" + log(method, "Schedule") + + return callWithExistingReactInstance(method) { reactInstance: ReactInstance -> + log(method, "Execute") + reactInstance.registerSegment(segmentId, path) + checkNotNull(callback).invoke() + } + } + + internal fun handleHostException(e: Exception): Unit { + val method = "handleHostException(message = \"${e.message}\")" + log(method) + + if (useDevSupport) { + devSupportManager.handleException(e) + } else { + reactHostDelegate.handleInstanceException(e) + } + destroy(method, 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. + */ + internal fun callFunctionOnModule( + moduleName: String, + methodName: String, + args: NativeArray + ): Task { + val method = "callFunctionOnModule(\"$moduleName\", \"$methodName\")" + return callWithExistingReactInstance(method) { reactInstance: ReactInstance -> + reactInstance.callFunctionOnModule(moduleName, methodName, args) + } + } + + internal fun attachSurface(surface: ReactSurfaceImpl) { + log("attachSurface(surfaceId = ${surface.surfaceID})") + synchronized(attachedSurfaces) { attachedSurfaces.add(surface) } + } + + internal fun detachSurface(surface: ReactSurfaceImpl) { + log("detachSurface(surfaceId = ${surface.surfaceID})") + synchronized(attachedSurfaces) { attachedSurfaces.remove(surface) } + } + + internal fun isSurfaceAttached(surface: ReactSurfaceImpl): Boolean = + synchronized(attachedSurfaces) { + return attachedSurfaces.contains(surface) + } + + internal fun isSurfaceWithModuleNameAttached(moduleName: String): Boolean = + synchronized(attachedSurfaces) { + return attachedSurfaces.any { surface -> surface.moduleName == moduleName } + } + + override fun addBeforeDestroyListener(onBeforeDestroy: () -> Unit) { + beforeDestroyListeners.add(onBeforeDestroy) + } + + override fun removeBeforeDestroyListener(onBeforeDestroy: () -> Unit) { + beforeDestroyListeners.remove(onBeforeDestroy) + } + + @ThreadConfined("ReactHost") private var startTask: Task? = null + + @ThreadConfined("ReactHost") + private fun getOrCreateStartTask(): Task { + startTask?.let { + return it + } + + val method = "getOrCreateStartTask()" + log(method, "Schedule") + if (ReactBuildConfig.DEBUG) { + Assertions.assertCondition( + ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture(), + "enableBridgelessArchitecture FeatureFlag must be set to start ReactNative.") + + Assertions.assertCondition( + ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer(), + "enableFabricRenderer FeatureFlag must be set to start ReactNative.") + + Assertions.assertCondition( + ReactNativeNewArchitectureFeatureFlags.useTurboModules(), + "useTurboModules FeatureFlag must be set to start ReactNative.") + } + if (ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE) { + Assertions.assertCondition( + !ReactNativeNewArchitectureFeatureFlags.useFabricInterop(), + "useFabricInterop FeatureFlag must be false when UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE == true.") + Assertions.assertCondition( + !ReactNativeNewArchitectureFeatureFlags.useTurboModuleInterop(), + "useTurboModuleInterop FeatureFlag must be false when UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE == true.") + } + + return waitThenCallGetOrCreateReactInstanceTask() + .continueWithTask( + { task -> + if (task.isFaulted()) { + val ex = checkNotNull(task.getError()) + if (useDevSupport) { + devSupportManager.handleException(ex) + } else { + reactHostDelegate.handleInstanceException(ex) + } + // Wait for destroy to finish + getOrCreateDestroyTask("getOrCreateStartTask() failure: ${ex.message}", ex) + .continueWithTask({ Task.forError(ex) }) + } else { + task.makeVoid() + } + }, + bgExecutor) + .also { startTask = it } + } + + @ThreadConfined(ThreadConfined.UI) + private fun moveToHostDestroy(currentContext: ReactContext?) { + reactLifecycleStateManager.moveToOnHostDestroy(currentContext) + currentActivity = null + } + + private fun raiseSoftException( + callingMethod: String, + message: String, + throwable: Throwable? = null + ) { + val method = "raiseSoftException($callingMethod)" + log(method, message) + ReactSoftExceptionLogger.logSoftException( + TAG, ReactNoCrashSoftException("$method: $message", throwable)) + } + + /** Schedule work on a ReactInstance that is already created. */ + private fun callWithExistingReactInstance( + callingMethod: String, + executor: Executor = Task.IMMEDIATE_EXECUTOR, + runnable: (reactInstance: ReactInstance) -> Unit, + ): Task = + createReactInstanceTaskRef + .get() + .onSuccess( + { task -> + val reactInstance = task.getResult() + if (reactInstance == null) { + raiseSoftException( + "callWithExistingReactInstance($callingMethod)", + "Execute: reactInstance is null. Dropping work.") + false + } else { + runnable(reactInstance) + true + } + }, + executor) + + /** Create a ReactInstance if it doesn't exist already, and schedule work on it. */ + private fun callAfterGetOrCreateReactInstance( + callingMethod: String, + executor: Executor = Task.IMMEDIATE_EXECUTOR, + runnable: (reactInstance: ReactInstance) -> Unit, + ): Task = + getOrCreateReactInstance() + .onSuccess( + { task -> + val reactInstance = task.getResult() + if (reactInstance == null) { + raiseSoftException( + "callAfterGetOrCreateReactInstance($callingMethod)", + "Execute: reactInstance is null. Dropping work.") + } else { + runnable(reactInstance) + } + null + }, + executor) + .continueWith({ task: Task -> + // TODO: validate whether errors during startup go through here? + if (task.isFaulted()) { + handleHostException(checkNotNull(task.getError())) + } + null + }) + + private fun getOrCreateReactContext(): BridgelessReactContext { + val method = "getOrCreateReactContext()" + return bridgelessReactContextRef.getOrCreate { + log(method, "Creating BridgelessReactContext") + BridgelessReactContext(context, 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. + */ + private fun getOrCreateReactInstance(): Task = + Task.call({ waitThenCallGetOrCreateReactInstanceTask() }, bgExecutor) + + @ThreadConfined("ReactHost") + private fun waitThenCallGetOrCreateReactInstanceTask(): Task = + waitThenCallGetOrCreateReactInstanceTaskWithRetries(0, 4) + + @ThreadConfined("ReactHost") + private fun waitThenCallGetOrCreateReactInstanceTaskWithRetries( + tryNum: Int, + maxTries: Int + ): Task { + val method = "waitThenCallGetOrCreateReactInstanceTaskWithRetries" + reloadTask?.let { task -> + log(method, "React Native is reloading. Return reload task.") + return task + } + + destroyTask?.let { task -> + val shouldTryAgain = tryNum < maxTries + if (shouldTryAgain) { + log( + method, + "React Native is tearing down.Wait for teardown to finish, before trying again (try count = $tryNum).") + return task.onSuccessTask( + { waitThenCallGetOrCreateReactInstanceTaskWithRetries(tryNum + 1, maxTries) }, + bgExecutor) + } + + raiseSoftException( + method, + "React Native is tearing down. Not wait for teardown to finish: reached max retries.") + } + + return getOrCreateReactInstanceTask() + } + + private class CreationResult( + val instance: ReactInstance, + val context: ReactContext, + val isReloading: Boolean + ) + + @ThreadConfined("ReactHost") + private fun getOrCreateReactInstanceTask(): Task { + val method = "getOrCreateReactInstanceTask()" + log(method) + + return createReactInstanceTaskRef.getOrCreate { + log(method, "Start") + Assertions.assertCondition( + !hostInvalidated, "Cannot start a new ReactInstance on an invalidated ReactHost") + + ReactMarker.logMarker( + ReactMarkerConstants.REACT_BRIDGELESS_LOADING_START, BRIDGELESS_MARKER_INSTANCE_KEY) + + val creationTask = + jsBundleLoader.onSuccess( + { task -> + val bundleLoader = checkNotNull(task.getResult()) + val reactContext = getOrCreateReactContext() + reactContext.setJSExceptionHandler(devSupportManager) + + log(method, "Creating ReactInstance") + val instance = + ReactInstance( + reactContext, + reactHostDelegate, + componentFactory, + devSupportManager, + { e: Exception -> this.handleHostException(e) }, + useDevSupport, + getOrCreateReactHostInspectorTarget()) + reactInstance = instance + + val memoryPressureListener = createMemoryPressureListener(instance) + this.memoryPressureListener = memoryPressureListener + memoryPressureRouter.addMemoryPressureListener(memoryPressureListener) + + // Eagerly initialize turbo modules in parallel with JS bundle execution + // as TurboModuleManager will handle any concurrent access + instance.initializeEagerTurboModules() + + 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 ensure that 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) + } + CreationResult(instance, reactContext, reloadTask != null) + }, + bgExecutor) + + val lifecycleUpdateTask = { task: Task -> + val result = checkNotNull(task.getResult()) + val reactInstance = result.instance + val reactContext = result.context + val isReloading = result.isReloading + val isManagerResumed = reactLifecycleStateManager.lifecycleState == LifecycleState.RESUMED + + /** + * ReactContext.onHostResume() should only be called when the user navigates to the first + * React Native screen. + * + * During init: The application puts the React manager in a resumed state, when the user + * navigates to a React Native screen. Two types of init: (1) If React Native init happens + * when the user navigates to a React Native screen, the React manager will get resumed on + * init start, so ReactContext.onHostResume() will be executed here. (2) If React Native + * init happens before the user navigates to a React Native screen (i.e: React Native is + * preloaded), the React manager won't be in a resumed state here. So + * ReactContext.onHostResume() won't be executed here. But, when the user navigates to their + * first React Native screen, the application will call ReactHost.onHostResume(). That will + * call ReactContext.onHostResume(). + * + * During reloads, if the manager isn't resumed, call ReactContext.onHostResume(). If React + * Native is reloading, it seems reasonable to assume that: (1) We must have navigated to a + * React Native screen in the past, or (2) We must be on a React Native screen. + */ + if (isReloading && !isManagerResumed) { + reactLifecycleStateManager.moveToOnHostResume(reactContext, currentActivity) + } else { + /** + * Call ReactContext.onHostResume() only when already in the resumed state which aligns + * with the bridge https://fburl.com/diffusion/2qhxmudv. + */ + reactLifecycleStateManager.resumeReactContextIfHostResumed(reactContext, currentActivity) + } + + log(method, "Executing ReactInstanceEventListeners") + for (listener in reactInstanceEventListeners) { + listener.onReactContextInitialized(reactContext) + } + reactInstance + } + + creationTask.onSuccess(lifecycleUpdateTask, uiExecutor) + creationTask.onSuccess({ task -> checkNotNull(task.getResult()).instance }) + } + } + + private val jsBundleLoader: Task + get() { + val method = "getJSBundleLoader()" + log(method) + + if (useDevSupport && allowPackagerServerAccess) { + return isMetroRunning.onSuccessTask( + { task -> + val isMetroRunning = checkNotNull(task.getResult()) + if (isMetroRunning) { + // Since metro is running, fetcxception(method, "ReactContext is null. Reload + // reason: $h the JS bundle from the server + loadJSBundleFromMetro() + } else { + Task.forResult(reactHostDelegate.jsBundleLoader) + } + }, + bgExecutor) + } else { + if (ReactBuildConfig.DEBUG) { + 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 try { + Task.forResult(reactHostDelegate.jsBundleLoader) + } catch (e: Exception) { + Task.forError(e) + } + } + } + + private val isMetroRunning: Task + get() { + val method = "isMetroRunning()" + log(method) + + val taskCompletionSource = TaskCompletionSource() + val asyncDevSupportManager = devSupportManager + + asyncDevSupportManager.isPackagerRunning { packagerIsRunning: Boolean -> + log(method, "Async result = $packagerIsRunning") + taskCompletionSource.setResult(packagerIsRunning) + } + + return taskCompletionSource.task + } + + private fun loadJSBundleFromMetro(): Task { + val method = "loadJSBundleFromMetro()" + log(method) + + val taskCompletionSource = TaskCompletionSource() + val asyncDevSupportManager = devSupportManager as DevSupportManagerBase + val bundleURL = + asyncDevSupportManager.devServerHelper.getDevServerBundleURL( + checkNotNull(asyncDevSupportManager.jsAppBundleName)) + + asyncDevSupportManager.reloadJSFromServer( + bundleURL, + object : BundleLoadCallback { + override fun onSuccess() { + log(method, "Creating BundleLoader") + val bundleLoader = + JSBundleLoader.createCachedBundleFromNetworkLoader( + bundleURL, asyncDevSupportManager.downloadedJSBundleFile) + taskCompletionSource.setResult(bundleLoader) + } + + override fun onError(cause: Exception) { + taskCompletionSource.setError(cause) + } + }) + + return taskCompletionSource.task + } + + private fun log(method: String, message: String) { + bridgelessReactStateTracker.enterState("ReactHost{$id}.$method: $message") + } + + private fun log(method: String) { + bridgelessReactStateTracker.enterState("ReactHost{$id}.$method") + } + + private fun stopAttachedSurfaces(method: String, reactInstance: ReactInstance) { + log(method, "Stopping all React Native surfaces") + synchronized(attachedSurfaces) { + for (surface in attachedSurfaces) { + reactInstance.stopSurface(surface) + surface.clear() + } + } + } + + private fun startAttachedSurfaces(method: String, reactInstance: ReactInstance) { + log(method, "Restarting previously running React Native Surfaces") + synchronized(attachedSurfaces) { + for (surface in attachedSurfaces) { + reactInstance.startSurface(surface) + } + } + } + + @ThreadConfined("ReactHost") private var reloadTask: Task? = null + + private fun createReactInstanceUnwrapper( + tag: String, + method: String, + reason: String + ): (task: Task, stage: String) -> ReactInstance? = + unwrap@{ task: Task, stage: String -> + val reactInstance = task.getResult() + val currentReactInstance = this.reactInstance + + val stageLabel = "Stage: $stage" + val reasonLabel = "$tag reason: $reason" + if (task.isFaulted()) { + val ex = checkNotNull(task.getError()) + val faultLabel = "Fault reason: ${ex.message}" + raiseSoftException( + method, "$tag: ReactInstance task faulted. $stageLabel. $faultLabel. $reasonLabel") + return@unwrap currentReactInstance + } + + if (task.isCancelled()) { + raiseSoftException( + method, "$tag: ReactInstance task cancelled. $stageLabel. $reasonLabel") + return@unwrap currentReactInstance + } + + if (reactInstance == null) { + raiseSoftException( + method, "$tag: ReactInstance task returned null. $stageLabel. $reasonLabel") + return@unwrap currentReactInstance + } + + if (currentReactInstance != null && reactInstance != currentReactInstance) { + raiseSoftException( + method, + ("$tag: Detected two different ReactInstances. Returning old. $stageLabel. $reasonLabel")) + } + reactInstance + } + + /** + * 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 fun getOrCreateReloadTask(reason: String): Task { + val method = "getOrCreateReloadTask()" + log(method) + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason) + + reloadTask?.let { + return it + } + + val taskUnwrapper = createReactInstanceUnwrapper("Reload", method, reason) + + // When using the immediate executor, we want to avoid scheduling any further work immediately + // when destruction is kicked off. + log(method, "Resetting createReactInstance task ref") + return createReactInstanceTaskRef.andReset + .continueWithTask( + { task -> + log(method, "Starting React Native reload") + val reactInstance = taskUnwrapper(task, "1: Starting reload") + + unregisterInstanceFromInspector(reactInstance) + + val reactContext = bridgelessReactContextRef.value + if (reactContext == null) { + raiseSoftException(method, "ReactContext is null. Reload reason: $reason") + } + + if (reactContext != null && + reactLifecycleStateManager.lifecycleState == LifecycleState.RESUMED) { + log(method, "Calling ReactContext.onHostPause()") + reactContext.onHostPause() + } + Task.forResult(reactInstance) + }, + uiExecutor) + .continueWithTask( + { task: Task -> + val reactInstance = taskUnwrapper(task, "2: Surface shutdown") + if (reactInstance == null) { + raiseSoftException(method, "Skipping surface shutdown: ReactInstance null") + } else { + stopAttachedSurfaces(method, reactInstance) + } + task + }, + bgExecutor) + .continueWithTask( + { task: Task -> + taskUnwrapper(task, "3: Destroying ReactContext") + for (destroyListener in beforeDestroyListeners) { + destroyListener.invoke() + } + + memoryPressureListener?.let { listener -> + log(method, "Removing memory pressure listener") + memoryPressureRouter.removeMemoryPressureListener(listener) + } + + val reactContext = bridgelessReactContextRef.value + if (reactContext != null) { + log(method, "Resetting ReactContext ref") + bridgelessReactContextRef.reset() + + log(method, "Destroying ReactContext") + reactContext.destroy() + } + + if (useDevSupport && reactContext != null) { + log(method, "Calling DevSupportManager.onReactInstanceDestroyed(reactContext)") + devSupportManager.onReactInstanceDestroyed(reactContext) + } + task + }, + uiExecutor) + .continueWithTask( + { task: Task -> + val reactInstance = taskUnwrapper(task, "4: Destroying ReactInstance") + if (reactInstance == null) { + raiseSoftException(method, "Skipping ReactInstance.destroy(): ReactInstance null") + } else { + log(method, "Resetting ReactInstance ptr") + this.reactInstance = null + + log(method, "Destroying ReactInstance") + reactInstance.destroy() + } + + log(method, "Resetting start task ref") + startTask = null + + // Kickstart a new ReactInstance create + getOrCreateReactInstanceTask() + }, + bgExecutor) + .continueWithTask( + { task: Task -> + val reactInstance = taskUnwrapper(task, "5: Restarting surfaces") + if (reactInstance == null) { + raiseSoftException(method, "Skipping surface restart: ReactInstance null") + } else { + startAttachedSurfaces(method, reactInstance) + } + task + }, + bgExecutor) + .continueWithTask( + { task: Task -> + if (task.isFaulted()) { + val fault = checkNotNull(task.getError()) + raiseSoftException( + method, + ("Error during reload. ReactInstance task faulted. Fault reason: ${fault.message}. Reload reason: $reason"), + task.getError()) + } + if (task.isCancelled()) { + raiseSoftException( + method, + "Error during reload. ReactInstance task cancelled. Reload reason: $reason") + } + + log(method, "Resetting reload task ref") + reloadTask = null + task + }, + bgExecutor) + .also { reloadTask = it } + } + + @ThreadConfined("ReactHost") private var destroyTask: Task? = 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 fun getOrCreateDestroyTask(reason: String, ex: Exception?): Task { + val method = "getOrCreateDestroyTask()" + log(method) + + // Log how React Native is destroyed + // TODO(T136397487): Remove after Venice is shipped to 100% + raiseSoftException(method, reason, ex) + + destroyTask?.let { + return it + } + + val taskUnwrapper = createReactInstanceUnwrapper("Destroy", method, reason) + + // When using the immediate executor, we want to avoid scheduling any further work immediately + // when destruction is kicked off. + log(method, "Resetting createReactInstance task ref") + return createReactInstanceTaskRef.andReset + .continueWithTask( + { task: Task -> + log(method, "Starting React Native destruction") + val reactInstance = taskUnwrapper(task, "1: Starting destroy") + + unregisterInstanceFromInspector(reactInstance) + + if (hostInvalidated) { + // If the host has been invalidated, now that the current context/instance + // has been unregistered, we can safely destroy the host's inspector + // target. + reactHostInspectorTarget?.close() + reactHostInspectorTarget = null + } + + // Step 1: Destroy DevSupportManager + if (useDevSupport) { + log(method, "DevSupportManager cleanup") + // TODO(T137233065): Disable DevSupportManager here + devSupportManager.stopInspector() + } + + val reactContext = bridgelessReactContextRef.value + if (reactContext == null) { + raiseSoftException(method, "ReactContext is null. Destroy reason: $reason") + } + + // Step 2: Move React Native to onHostDestroy() + log(method, "Move ReactHost to onHostDestroy()") + reactLifecycleStateManager.moveToOnHostDestroy(reactContext) + Task.forResult(reactInstance) + }, + uiExecutor) + .continueWithTask( + { task: Task -> + val reactInstance = taskUnwrapper(task, "2: Stopping surfaces") + if (reactInstance == null) { + raiseSoftException(method, "Skipping surface shutdown: ReactInstance null") + } else { + // Step 3: Stop all React Native surfaces + stopAttachedSurfaces(method, reactInstance) + synchronized(attachedSurfaces) { attachedSurfaces.clear() } + } + task + }, + bgExecutor) + .continueWithTask( + { task: Task -> + taskUnwrapper(task, "3: Destroying ReactContext") + for (destroyListener in beforeDestroyListeners) { + destroyListener.invoke() + } + + val reactContext = bridgelessReactContextRef.value + if (reactContext == null) { + raiseSoftException(method, "ReactContext is null. Destroy reason: $reason") + } + + // Step 4: De-register the memory pressure listener + log(method, "Destroying MemoryPressureRouter") + memoryPressureRouter.destroy(context) + + if (reactContext != null) { + log(method, "Resetting ReactContext ref") + bridgelessReactContextRef.reset() + + log(method, "Destroying ReactContext") + reactContext.destroy() + } + + // Reset current activity + currentActivity = null + + // Clear ResourceIdleDrawableIdMap + ResourceDrawableIdHelper.clear() + task + }, + uiExecutor) + .continueWithTask( + { task: Task -> + val reactInstance = taskUnwrapper(task, "4: Destroying ReactInstance") + if (reactInstance == null) { + raiseSoftException(method, "Skipping ReactInstance.destroy(): ReactInstance null") + } else { + log(method, "Resetting ReactInstance ptr") + this.reactInstance = null + + log(method, "Destroying ReactInstance") + reactInstance.destroy() + } + + log(method, "Resetting start task ref") + startTask = null + + log(method, "Resetting destroy task ref") + destroyTask = null + task + }, + bgExecutor) + .continueWith({ task: Task -> + if (task.isFaulted()) { + val fault = checkNotNull(task.getError()) + raiseSoftException( + method, + ("React destruction failed. ReactInstance task faulted. Fault reason: ${fault.message}. Destroy reason: $reason"), + task.getError()) + } + if (task.isCancelled()) { + raiseSoftException( + method, + "React destruction failed. ReactInstance task cancelled. Destroy reason: $reason") + } + null + }) + .also { destroyTask = it } + } + + internal fun getOrCreateReactHostInspectorTarget(): ReactHostInspectorTarget? { + if (reactHostInspectorTarget == null && InspectorFlags.getFuseboxEnabled()) { + // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. + reactHostInspectorTarget = ReactHostInspectorTarget(this) + } + + return reactHostInspectorTarget + } + + @ThreadConfined(ThreadConfined.UI) + internal fun unregisterInstanceFromInspector(reactInstance: ReactInstance?): Unit { + if (reactInstance != null) { + if (InspectorFlags.getFuseboxEnabled()) { + Assertions.assertCondition( + reactHostInspectorTarget?.isValid() == true, + "Host inspector target destroyed before instance was unregistered") + } + reactInstance.unregisterFromInspector() + } + } + + override fun invalidate() { + FLog.d(TAG, "ReactHostImpl.invalidate()") + hostInvalidated = true + destroy("ReactHostImpl.invalidate()", null) + } + + private companion object { + private const val TAG = "ReactHost" + private const val BRIDGELESS_MARKER_INSTANCE_KEY = 1 + private val counter = AtomicInteger(0) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt index 91351a09f89..e18982a9a8f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt @@ -13,6 +13,8 @@ import android.view.View import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.bridge.JavaScriptExecutorFactory import com.facebook.react.bridge.ReactContext +import com.facebook.react.common.annotations.FrameworkAPI +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.ReactInstanceDevHelper import com.facebook.react.interfaces.TaskInterface import com.facebook.react.modules.core.DeviceEventManagerModule @@ -23,6 +25,8 @@ import com.facebook.react.modules.core.DeviceEventManagerModule * This allows [BridgelessDevSupportHelper] and other classes inside the .devsupport package to * communicate with the Bridgeless infrastructure without exposing public APIs. */ +@UnstableReactNativeAPI +@OptIn(FrameworkAPI::class) internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : ReactInstanceDevHelper { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt index 2a7de5125dc..00d70dbd75e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt @@ -10,11 +10,15 @@ package com.facebook.react.runtime import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStripAny import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.annotations.FrameworkAPI +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.soloader.SoLoader import java.io.Closeable import java.util.concurrent.Executor @DoNotStripAny +@UnstableReactNativeAPI +@OptIn(FrameworkAPI::class) internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) : Closeable { // fbjni looks for the exact name "mHybridData": // https://github.com/facebookincubator/fbjni/blob/5587a7fd2b191656be9391a3832ce04c034009a5/cxx/fbjni/detail/Hybrid.h#L310 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.kt index a9cea204fbb..e4868c7a9e7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.kt @@ -18,6 +18,8 @@ import com.facebook.react.ReactHost import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.NativeMap import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.annotations.FrameworkAPI +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.fabric.SurfaceHandlerBinding import com.facebook.react.interfaces.TaskInterface @@ -30,6 +32,7 @@ import java.util.concurrent.atomic.AtomicReference /** A class responsible for creating and rendering a full-screen React surface. */ @ThreadSafe +@OptIn(FrameworkAPI::class, UnstableReactNativeAPI::class) public class ReactSurfaceImpl @VisibleForTesting internal constructor( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt index 0ee27bc3b1c..f57edbe1b7e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt @@ -17,6 +17,8 @@ import android.view.View import com.facebook.common.logging.FLog import com.facebook.react.ReactRootView import com.facebook.react.bridge.ReactContext +import com.facebook.react.common.annotations.FrameworkAPI +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.config.ReactFeatureFlags import com.facebook.react.uimanager.IllegalViewOperationException import com.facebook.react.uimanager.JSPointerDispatcher @@ -29,6 +31,7 @@ import java.util.Objects * A view created by [com.facebook.react.interfaces.fabric.ReactSurface] that's responsible for * rendering a React component. */ +@OptIn(FrameworkAPI::class, UnstableReactNativeAPI::class) public class ReactSurfaceView(context: Context?, private val surface: ReactSurfaceImpl) : ReactRootView(context) { private val jsTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt index 68f5dcb58f0..72a1b57c0ca 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt @@ -12,7 +12,7 @@ package com.facebook.react.runtime import android.app.Activity import android.content.Context import com.facebook.react.bridge.WritableNativeArray -import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.fabric.FabricUIManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.uimanager.UIManagerModule @@ -46,7 +46,7 @@ import org.robolectric.annotation.Config ShadowNativeLoader::class, ShadowArguments::class, ShadowWritableNativeArray::class]) -@OptIn(UnstableReactNativeAPI::class) +@OptIn(FrameworkAPI::class) class BridgelessReactContextTest { private lateinit var context: Context private lateinit var reactHost: ReactHostImpl