From 7a18e0522c3231dc3ac4d7118a253c2559781005 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Wed, 16 Oct 2024 07:29:09 -0700 Subject: [PATCH] Move BridgelessDevSupportManager to .devsupport package (#46914) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46914 The `BridgelessDevSupportManager` should have lived inside the `.devsupport` package alongside all the other devsupport related tooling. It was instead created inside `.runtime` causing a tight coupling with `ReactHostImpl`. This made it impossible for Frameworks to customize it (i.e. in Expo GO) also because there was a circular dependency between ReactHostImpl and BridgelessDevSupportManager In this diff I'm: 1. Breaking the circular dependency by using `ReactHostDevHelper` 2. Updating all the parameters to reference `ReactHost` rather than `ReactHostImpl` 3. Moving BridgelessDevSupportManager to the `.devsupport` package. This is breaking for users that are manually composing a `BridgelessDevSupportManager` or that are extending the `ReactInstanceDevHelper`. - `ReactInstanceDevHelper` has 3 new method which will have to be implemented. - `BridgelessDevSupportManager` is now living in a different package. Changelog: [Android] [Breaking] - Add 3 methods to ReactInstanceDevHelper Reviewed By: rshest Differential Revision: D64105790 --- .../ReactAndroid/api/ReactAndroid.api | 9 +- .../facebook/react/ReactInstanceManager.java | 18 ++ .../BridgelessDevSupportManager.java | 123 ++++++++++++++ .../DefaultDevSupportManagerFactory.kt | 34 +++- .../devsupport/DevSupportManagerBase.java | 2 +- .../devsupport/DevSupportManagerFactory.java | 22 +++ .../devsupport/ReactInstanceDevHelper.java | 14 +- .../runtime/BridgelessDevSupportManager.java | 159 ------------------ .../facebook/react/runtime/ReactHostImpl.java | 49 +++++- .../react/runtime/ReactHostImplDevHelper.kt | 75 +++++++++ .../react/runtime/ReactSurfaceImpl.java | 8 +- .../facebook/react/runtime/ReactHostTest.kt | 22 +-- 12 files changed, 347 insertions(+), 188 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessDevSupportManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0e84d913e05469..c33c2fc284df24 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2134,6 +2134,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa public final class com/facebook/react/devsupport/DefaultDevSupportManagerFactory : com/facebook/react/devsupport/DevSupportManagerFactory { public fun ()V public fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; + public fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;Z)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; } public class com/facebook/react/devsupport/DevServerHelper { @@ -2173,6 +2174,7 @@ public final class com/facebook/react/devsupport/DevSettingsActivity : android/p } public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/facebook/react/devsupport/interfaces/DevSupportManager { + protected final field mReactInstanceDevHelper Lcom/facebook/react/devsupport/ReactInstanceDevHelper; public fun (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)V public fun addCustomDevOption (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/DevOptionHandler;)V public fun createRootView (Ljava/lang/String;)Landroid/view/View; @@ -2236,6 +2238,7 @@ public abstract interface class com/facebook/react/devsupport/DevSupportManagerB public abstract interface class com/facebook/react/devsupport/DevSupportManagerFactory { public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; + public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;Z)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; } public final class com/facebook/react/devsupport/DoubleTapReloadRecognizer { @@ -2327,9 +2330,12 @@ public abstract interface class com/facebook/react/devsupport/ReactInstanceDevHe public abstract fun createRootView (Ljava/lang/String;)Landroid/view/View; public abstract fun destroyRootView (Landroid/view/View;)V public abstract fun getCurrentActivity ()Landroid/app/Activity; + public abstract fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext; public abstract fun getJavaScriptExecutorFactory ()Lcom/facebook/react/bridge/JavaScriptExecutorFactory; + public abstract fun loadBundle (Lcom/facebook/react/bridge/JSBundleLoader;)Lcom/facebook/react/interfaces/TaskInterface; public abstract fun onJSBundleLoadedFromServer ()V public abstract fun onReloadWithJSDebugger (Lcom/facebook/react/bridge/JavaJSExecutor$Factory;)V + public abstract fun reload (Ljava/lang/String;)V public abstract fun toggleElementInspector ()V } @@ -3887,6 +3893,7 @@ public abstract class com/facebook/react/runtime/JSRuntimeFactory { 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 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 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 @@ -3918,7 +3925,7 @@ public class com/facebook/react/runtime/ReactHostImpl : com/facebook/react/React public class com/facebook/react/runtime/ReactSurfaceImpl : com/facebook/react/interfaces/fabric/ReactSurface { public fun (Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)V - public fun attach (Lcom/facebook/react/runtime/ReactHostImpl;)V + public fun attach (Lcom/facebook/react/ReactHost;)V public fun attachView (Lcom/facebook/react/runtime/ReactSurfaceView;)V public fun clear ()V public static fun createWithView (Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Lcom/facebook/react/runtime/ReactSurfaceImpl; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index bc190d6f6e6c62..dc07b2bfaaf093 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -92,6 +92,7 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager; import com.facebook.react.devsupport.interfaces.RedBoxHandler; +import com.facebook.react.interfaces.TaskInterface; import com.facebook.react.internal.AndroidChoreographerProvider; import com.facebook.react.internal.ChoreographerProvider; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; @@ -369,6 +370,23 @@ public void destroyRootView(View rootView) { ((ReactRootView) rootView).unmountReactApplication(); } } + + @Override + public void reload(String s) { + // no-op not implemented for Bridge Mode + } + + @Override + public TaskInterface loadBundle(JSBundleLoader bundleLoader) { + // no-op not implemented for Bridge Mode + return null; + } + + @Override + public ReactContext getCurrentReactContext() { + // no-op not implemented for Bridge Mode + return null; + } }; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.java new file mode 100644 index 00000000000000..676a8620e0d397 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.java @@ -0,0 +1,123 @@ +/* + * 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.devsupport; + +import android.content.Context; +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.SurfaceDelegateFactory; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; +import com.facebook.react.devsupport.interfaces.DevLoadingViewManager; +import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; +import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager; +import com.facebook.react.devsupport.interfaces.RedBoxHandler; +import com.facebook.react.packagerconnection.RequestHandler; +import java.util.Map; + +/** + * An implementation of {@link com.facebook.react.devsupport.interfaces.DevSupportManager} that + * extends the functionality in {@link DevSupportManagerBase} with some additional, more flexible + * APIs for asynchronously loading the JS bundle. + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +class BridgelessDevSupportManager extends DevSupportManagerBase { + + public BridgelessDevSupportManager( + Context context, + ReactInstanceDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName) { + this( + context.getApplicationContext(), + reactInstanceManagerHelper, + packagerPathForJSBundleName, + true /* enableOnCreate */, + null /* redBoxHandler */, + null /* devBundleDownloadListener */, + 2 /* minNumShakes */, + null /* customPackagerCommandHandlers */, + null /* surfaceDelegateFactory */, + null /* devLoadingViewManager */, + null /* pausedInDebuggerOverlayManager */); + } + + /** + * This constructor mirrors the same constructor we have for {@link BridgeDevSupportManager} and + * is kept for backward compatibility. + */ + public BridgelessDevSupportManager( + Context applicationContext, + ReactInstanceDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler, + @Nullable DevBundleDownloadListener devBundleDownloadListener, + int minNumShakes, + @Nullable Map customPackagerCommandHandlers, + @Nullable SurfaceDelegateFactory surfaceDelegateFactory, + @Nullable DevLoadingViewManager devLoadingViewManager, + @Nullable PausedInDebuggerOverlayManager pausedInDebuggerOverlayManager) { + super( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers, + surfaceDelegateFactory, + devLoadingViewManager, + pausedInDebuggerOverlayManager); + } + + @Override + protected String getUniqueTag() { + return "Bridgeless"; + } + + @Override + public void loadSplitBundleFromServer( + final String bundlePath, final DevSplitBundleCallback callback) { + fetchSplitBundleAndCreateBundleLoader( + bundlePath, + new CallbackWithBundleLoader() { + @Override + public void onSuccess(final JSBundleLoader bundleLoader) { + try { + mReactInstanceDevHelper.loadBundle(bundleLoader).waitForCompletion(); + String bundleURL = getDevServerHelper().getDevServerSplitBundleURL(bundlePath); + ReactContext reactContext = mReactInstanceDevHelper.getCurrentReactContext(); + if (reactContext != null) { + reactContext.getJSModule(HMRClient.class).registerBundle(bundleURL); + } + callback.onSuccess(); + } catch (InterruptedException e) { + throw new RuntimeException( + "[BridgelessDevSupportManager]: Got interrupted while loading bundle", e); + } + } + + @Override + public void onError(String url, Throwable cause) { + callback.onError(url, cause); + } + }); + } + + @Override + public void handleReloadJS() { + UiThreadUtil.assertOnUiThread(); + + // dismiss redbox if exists + hideRedboxDialog(); + mReactInstanceDevHelper.reload("BridgelessDevSupportManager.handleReloadJS()"); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevSupportManagerFactory.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevSupportManagerFactory.kt index 9ca3b860bf48cb..068b1204fc6af6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevSupportManagerFactory.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevSupportManagerFactory.kt @@ -19,7 +19,8 @@ import com.facebook.react.packagerconnection.RequestHandler /** * A simple factory that creates instances of [DevSupportManager] implementations. Uses reflection * to create BridgeDevSupportManager if it exists. This allows ProGuard to strip that class and its - * dependencies in release builds. If the class isn't found, [ ] is returned instead. + * dependencies in release builds. If the class isn't found, [PerftestDevSupportManager] is returned + * instead. */ public class DefaultDevSupportManagerFactory : DevSupportManagerFactory { @@ -85,6 +86,37 @@ public class DefaultDevSupportManagerFactory : DevSupportManagerFactory { } } + override fun create( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: MutableMap?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?, + useDevSupport: Boolean + ): DevSupportManager = + if (!useDevSupport) { + ReleaseDevSupportManager() + } else { + BridgelessDevSupportManager( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers, + surfaceDelegateFactory, + devLoadingViewManager, + pausedInDebuggerOverlayManager) + } + private companion object { private const val DEVSUPPORT_IMPL_PACKAGE = "com.facebook.react.devsupport" private const val DEVSUPPORT_IMPL_CLASS = "BridgeDevSupportManager" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java index cc211ab0081d70..35dba5ee6e3e26 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java @@ -94,7 +94,7 @@ public interface CallbackWithBundleLoader { private final BroadcastReceiver mReloadAppBroadcastReceiver; private final DevServerHelper mDevServerHelper; private final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); - private final ReactInstanceDevHelper mReactInstanceDevHelper; + protected final ReactInstanceDevHelper mReactInstanceDevHelper; private final @Nullable String mJSAppBundleName; private final File mJSBundleDownloadedFile; private final File mJSSplitBundlesDir; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java index 9d0d120a16b6b6..29d9f7e3053be9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java @@ -19,6 +19,10 @@ import java.util.Map; public interface DevSupportManagerFactory { + /** + * Factory used by the Old Architecture flow to create a {@link DevSupportManager} and a {@link + * com.facebook.react.runtime.BridgeDevSupportManager} + */ DevSupportManager create( Context applicationContext, ReactInstanceDevHelper reactInstanceManagerHelper, @@ -31,4 +35,22 @@ DevSupportManager create( @Nullable SurfaceDelegateFactory surfaceDelegateFactory, @Nullable DevLoadingViewManager devLoadingViewManager, @Nullable PausedInDebuggerOverlayManager pausedInDebuggerOverlayManager); + + /** + * Factory used by the New Architecture/Bridgeless flow to create a {@link DevSupportManager} and + * a {@link BridgelessDevSupportManager} + */ + DevSupportManager create( + Context applicationContext, + ReactInstanceDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler, + @Nullable DevBundleDownloadListener devBundleDownloadListener, + int minNumShakes, + @Nullable Map customPackagerCommandHandlers, + @Nullable SurfaceDelegateFactory surfaceDelegateFactory, + @Nullable DevLoadingViewManager devLoadingViewManager, + @Nullable PausedInDebuggerOverlayManager pausedInDebuggerOverlayManager, + boolean useDevSupport); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.java index d418c5f14da4ce..8d6e9570b29fb7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.java @@ -10,13 +10,16 @@ import android.app.Activity; import android.view.View; import androidx.annotation.Nullable; +import com.facebook.react.bridge.JSBundleLoader; import com.facebook.react.bridge.JavaJSExecutor; import com.facebook.react.bridge.JavaScriptExecutorFactory; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.interfaces.TaskInterface; /** * Interface used by {@link DevSupportManager} for accessing some fields and methods of {@link - * ReactInstanceManager} or {@link ReactHostImpl} for the purpose of displaying and handling - * developer menu options. + * ReactInstanceManager} or {@link ReactHost} for the purpose of displaying and handling developer + * menu options. */ public interface ReactInstanceDevHelper { @@ -39,4 +42,11 @@ public interface ReactInstanceDevHelper { View createRootView(String appKey); void destroyRootView(View rootView); + + void reload(String s); + + TaskInterface loadBundle(JSBundleLoader bundleLoader); + + @Nullable + ReactContext getCurrentReactContext(); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessDevSupportManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessDevSupportManager.java deleted file mode 100644 index b411f73f9e57dc..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessDevSupportManager.java +++ /dev/null @@ -1,159 +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 android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.JSBundleLoader; -import com.facebook.react.bridge.JavaJSExecutor; -import com.facebook.react.bridge.JavaScriptExecutorFactory; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.devsupport.DevSupportManagerBase; -import com.facebook.react.devsupport.HMRClient; -import com.facebook.react.devsupport.ReactInstanceDevHelper; -import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.facebook.react.runtime.internal.bolts.Continuation; -import com.facebook.react.runtime.internal.bolts.Task; - -/** - * An implementation of {@link com.facebook.react.devsupport.interfaces.DevSupportManager} that - * extends the functionality in {@link DevSupportManagerBase} with some additional, more flexible - * APIs for asynchronously loading the JS bundle. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -class BridgelessDevSupportManager extends DevSupportManagerBase { - - private final ReactHostImpl mReactHost; - - public BridgelessDevSupportManager( - ReactHostImpl host, Context context, @Nullable String packagerPathForJSBundleName) { - super( - context.getApplicationContext(), - createInstanceDevHelper(host), - packagerPathForJSBundleName, - true /* enableOnCreate */, - null /* redBoxHandler */, - null /* devBundleDownloadListener */, - 2 /* minNumShakes */, - null /* customPackagerCommandHandlers */, - null /* surfaceDelegateFactory */, - null /* devLoadingViewManager */, - null /* pausedInDebuggerOverlayManager */); - mReactHost = host; - } - - @Override - protected String getUniqueTag() { - return "Bridgeless"; - } - - @Override - public void loadSplitBundleFromServer( - final String bundlePath, final DevSplitBundleCallback callback) { - fetchSplitBundleAndCreateBundleLoader( - bundlePath, - new CallbackWithBundleLoader() { - @Override - public void onSuccess(final JSBundleLoader bundleLoader) { - mReactHost - .loadBundle(bundleLoader) - .onSuccess( - new Continuation() { - @Override - public Void then(Task task) { - if (task.getResult().equals(Boolean.TRUE)) { - String bundleURL = - getDevServerHelper().getDevServerSplitBundleURL(bundlePath); - ReactContext reactContext = mReactHost.getCurrentReactContext(); - if (reactContext != null) { - reactContext.getJSModule(HMRClient.class).registerBundle(bundleURL); - } - callback.onSuccess(); - } - return null; - } - }); - } - - @Override - public void onError(String url, Throwable cause) { - callback.onError(url, cause); - } - }); - } - - @Override - public void handleReloadJS() { - UiThreadUtil.assertOnUiThread(); - - // dismiss redbox if exists - hideRedboxDialog(); - mReactHost.reload("BridgelessDevSupportManager.handleReloadJS()"); - } - - private static ReactInstanceDevHelper createInstanceDevHelper(final ReactHostImpl reactHost) { - return new ReactInstanceDevHelper() { - @Override - public void onReloadWithJSDebugger(JavaJSExecutor.Factory proxyExecutorFactory) { - // Not implemented, only used by BridgeDevSupportManager to reload with proxy executor - } - - @Override - public void onJSBundleLoadedFromServer() { - // Not implemented, only referenced by BridgeDevSupportManager - } - - @Override - public void toggleElementInspector() { - ReactContext reactContext = reactHost.getCurrentReactContext(); - if (reactContext != null) { - reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("toggleElementInspector", null); - } - } - - @Nullable - @Override - public Activity getCurrentActivity() { - return reactHost.getLastUsedActivity(); - } - - @Override - public JavaScriptExecutorFactory getJavaScriptExecutorFactory() { - throw new IllegalStateException("Not implemented for bridgeless mode"); - } - - @Nullable - @Override - public View createRootView(String appKey) { - Activity currentActivity = getCurrentActivity(); - if (currentActivity != null && !reactHost.isSurfaceWithModuleNameAttached(appKey)) { - ReactSurfaceImpl reactSurface = - ReactSurfaceImpl.createWithView(currentActivity, appKey, new Bundle()); - reactSurface.attach(reactHost); - reactSurface.start(); - - return reactSurface.getView(); - } - return null; - } - - @Override - public void destroyRootView(View rootView) { - // Not implemented - } - }; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java index 2a858271956d2c..a5732d3c893c13 100644 --- 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 @@ -46,9 +46,10 @@ 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.ReleaseDevSupportManager; import com.facebook.react.devsupport.inspector.InspectorNetworkHelper; import com.facebook.react.devsupport.inspector.InspectorNetworkRequestListener; import com.facebook.react.devsupport.interfaces.BundleLoadCallback; @@ -106,7 +107,7 @@ public class ReactHostImpl implements ReactHost { private final Context mContext; private final ReactHostDelegate mReactHostDelegate; private final ComponentFactory mComponentFactory; - private final DevSupportManager mDevSupportManager; + private DevSupportManager mDevSupportManager; private final Executor mBGExecutor; private final Executor mUIExecutor; private final Set mAttachedSurfaces = new HashSet<>(); @@ -165,6 +166,26 @@ public ReactHostImpl( 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; @@ -173,14 +194,24 @@ public ReactHostImpl( mMemoryPressureRouter = new MemoryPressureRouter(context); mAllowPackagerServerAccess = allowPackagerServerAccess; mUseDevSupport = useDevSupport; - - if (mUseDevSupport) { - mDevSupportManager = - new BridgelessDevSupportManager( - ReactHostImpl.this, mContext, mReactHostDelegate.getJsMainModulePath()); - } else { - mDevSupportManager = new ReleaseDevSupportManager(); + 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 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 new file mode 100644 index 00000000000000..d0e67813ec78f6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt @@ -0,0 +1,75 @@ +/* + * 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.os.Bundle +import android.view.View +import com.facebook.react.bridge.JSBundleLoader +import com.facebook.react.bridge.JavaJSExecutor +import com.facebook.react.bridge.JavaScriptExecutorFactory +import com.facebook.react.bridge.ReactContext +import com.facebook.react.devsupport.ReactInstanceDevHelper +import com.facebook.react.interfaces.TaskInterface +import com.facebook.react.modules.core.DeviceEventManagerModule + +/** + * Implementation of [ReactInstanceDevHelper] for [ReactHostImpl]. + * + * This allows [BridgelessDevSupportHelper] and other classes inside the .devsupport package to + * communicate with the Bridgeless infrastructure without exposing public APIs. + */ +internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : + ReactInstanceDevHelper { + + override fun onReloadWithJSDebugger(proxyExecutorFactory: JavaJSExecutor.Factory) { + // Not implemented, only used by BridgeDevSupportManager to reload with proxy executor + } + + override fun onJSBundleLoadedFromServer() { + // Not implemented, only referenced by BridgeDevSupportManager + } + + override fun toggleElementInspector() { + val reactContext = delegate.currentReactContext + reactContext + ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + ?.emit("toggleElementInspector", null) + } + + override fun getCurrentActivity(): Activity? = delegate.lastUsedActivity + + override fun getJavaScriptExecutorFactory(): JavaScriptExecutorFactory { + throw IllegalStateException("Not implemented for bridgeless mode") + } + + override fun createRootView(appKey: String): View? { + val currentActivity = currentActivity + if (currentActivity != null && !delegate.isSurfaceWithModuleNameAttached(appKey)) { + val reactSurface = ReactSurfaceImpl.createWithView(currentActivity, appKey, Bundle()) + reactSurface.attach(delegate) + reactSurface.start() + + return reactSurface.view + } + return null + } + + override fun destroyRootView(rootView: View) { + // Not implemented, only referenced by BridgeDevSupportManager + } + + override fun reload(s: String) { + delegate.reload(s) + } + + override fun loadBundle(bundleLoader: JSBundleLoader): TaskInterface = + delegate.loadBundle(bundleLoader) + + override fun getCurrentReactContext(): ReactContext? = delegate.currentReactContext +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.java index 89ed46fb9a2ada..f68b13bc2d18a5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceImpl.java @@ -16,6 +16,7 @@ import androidx.annotation.UiThread; import com.facebook.infer.annotation.Nullsafe; import com.facebook.infer.annotation.ThreadSafe; +import com.facebook.react.ReactHost; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.NativeMap; import com.facebook.react.bridge.UiThreadUtil; @@ -93,9 +94,12 @@ public ReactSurfaceImpl(Context context, String moduleName, @Nullable Bundle ini * * @param host The ReactHost to attach. */ - public void attach(ReactHostImpl host) { - if (!mReactHost.compareAndSet(null, host)) { + public void attach(ReactHost host) { + if (host instanceof ReactHostImpl && !mReactHost.compareAndSet(null, (ReactHostImpl) host)) { throw new IllegalStateException("This surface is already attached to a host!"); + } else if (!(host instanceof ReactHostImpl)) { + throw new IllegalArgumentException( + "ReactSurfaceImpl.attach can only attach to ReactHostImpl."); } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt index ef83a8690b1c29..59a895c8c03b95 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt @@ -9,6 +9,7 @@ package com.facebook.react.runtime import android.app.Activity import android.os.Looper +import com.facebook.react.BuildConfig import com.facebook.react.MemoryPressureRouter import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.bridge.MemoryPressureListener @@ -16,7 +17,6 @@ import com.facebook.react.bridge.UIManager import com.facebook.react.common.LifecycleState import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.ReleaseDevSupportManager -import com.facebook.react.devsupport.interfaces.PackagerStatusCallback import com.facebook.react.fabric.ComponentFactory import com.facebook.react.interfaces.TaskInterface import com.facebook.react.runtime.internal.bolts.TaskCompletionSource @@ -58,7 +58,6 @@ class ReactHostTest { private lateinit var mockedReactInstanceCtor: MockedConstruction private lateinit var mockedReactHostInspectorTargetCtor: MockedConstruction - private lateinit var mockedDevSupportManagerCtor: MockedConstruction private lateinit var mockedBridgelessReactContextCtor: MockedConstruction private lateinit var mockedMemoryPressureRouterCtor: MockedConstruction private lateinit var mockedTaskCompletionSourceCtor: MockedConstruction> @@ -77,14 +76,17 @@ class ReactHostTest { mockedReactInstanceCtor = Mockito.mockConstruction(ReactInstance::class.java) mockedReactHostInspectorTargetCtor = Mockito.mockConstruction(ReactHostInspectorTarget::class.java) - mockedDevSupportManagerCtor = Mockito.mockConstruction(BridgelessDevSupportManager::class.java) mockedBridgelessReactContextCtor = Mockito.mockConstruction(BridgelessReactContext::class.java) mockedMemoryPressureRouterCtor = Mockito.mockConstruction(MemoryPressureRouter::class.java) Mockito.doReturn(jSBundleLoader).`when`(reactHostDelegate).jsBundleLoader reactHost = ReactHostImpl( - activityController.get().application, reactHostDelegate, componentFactory, false, false) + activityController.get().application, + reactHostDelegate, + componentFactory, + false, + BuildConfig.DEBUG) val taskCompletionSource = TaskCompletionSource().apply { setResult(true) } mockedTaskCompletionSourceCtor = Mockito.mockConstruction( @@ -95,7 +97,6 @@ class ReactHostTest { fun tearDown() { mockedReactInstanceCtor.close() mockedReactHostInspectorTargetCtor.close() - mockedDevSupportManagerCtor.close() mockedBridgelessReactContextCtor.close() mockedMemoryPressureRouterCtor.close() mockedTaskCompletionSourceCtor.close() @@ -117,9 +118,9 @@ class ReactHostTest { fun testGetDevSupportManager() { // BridgelessDevSupportManager is created only for debug // we check if it was instantiated or if ReleaseDevSupportManager was created (for release). - if (mockedDevSupportManagerCtor.constructed().isNotEmpty()) { - val devSupportManager = mockedDevSupportManagerCtor.constructed().first() - Assertions.assertThat(reactHost.devSupportManager).isEqualTo(devSupportManager) + if (BuildConfig.DEBUG) { + Assertions.assertThat(reactHost.devSupportManager) + .isNotInstanceOf(ReleaseDevSupportManager::class.java) } else { Assertions.assertThat(reactHost.devSupportManager) .isInstanceOf(ReleaseDevSupportManager::class.java) @@ -129,11 +130,6 @@ class ReactHostTest { @Test @Ignore("Test is currently failing in OSS and needs to be looked into") fun testStart() { - val devSupportManager = mockedDevSupportManagerCtor.constructed().first() - - Mockito.doNothing() - .`when`(devSupportManager) - .isPackagerRunning(ArgumentMatchers.any(PackagerStatusCallback::class.java)) Assertions.assertThat(reactHost.isInstanceInitialized).isFalse() waitForTaskUIThread(reactHost.start()) Assertions.assertThat(reactHost.isInstanceInitialized).isTrue()