diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 94f52d109ec4a7..b88f3cf7b401b0 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -12,7 +12,7 @@ public class com/facebook/react/CoreModulesPackage$$ReactModuleInfoProvider : co public fun getReactModuleInfos ()Ljava/util/Map; } -public class com/facebook/react/DebugCorePackage : com/facebook/react/BaseReactPackage, com/facebook/react/ViewManagerOnDemandReactPackage { +public final class com/facebook/react/DebugCorePackage : com/facebook/react/BaseReactPackage, com/facebook/react/ViewManagerOnDemandReactPackage { public fun ()V public fun createViewManager (Lcom/facebook/react/bridge/ReactApplicationContext;Ljava/lang/String;)Lcom/facebook/react/uimanager/ViewManager; public fun getModule (Ljava/lang/String;Lcom/facebook/react/bridge/ReactApplicationContext;)Lcom/facebook/react/bridge/NativeModule; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java deleted file mode 100644 index 41d9267190834e..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java +++ /dev/null @@ -1,86 +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; - -import androidx.annotation.Nullable; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.ModuleSpec; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.UIManager; -import com.facebook.react.module.annotations.ReactModuleList; -import com.facebook.react.module.model.ReactModuleInfo; -import com.facebook.react.module.model.ReactModuleInfoProvider; -import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.views.debuggingoverlay.DebuggingOverlayManager; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Package defining core framework modules (e.g. UIManager). It should be used for modules that - * require special integration with other framework parts (e.g. with the list of packages to load - * view managers from). - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -@ReactModuleList(nativeModules = {}) -/* package */ -public class DebugCorePackage extends BaseReactPackage implements ViewManagerOnDemandReactPackage { - private @Nullable Map mViewManagers; - - public DebugCorePackage() {} - - @Override - public @Nullable NativeModule getModule(String name, ReactApplicationContext reactContext) { - return null; - } - - @Override - public ReactModuleInfoProvider getReactModuleInfoProvider() { - return new ReactModuleInfoProvider() { - @Override - public Map getReactModuleInfos() { - return Collections.emptyMap(); - } - }; - } - - /** - * @return a map of view managers that should be registered with {@link UIManager} - */ - private Map getViewManagersMap() { - if (mViewManagers == null) { - Map viewManagers = new HashMap<>(); - viewManagers.put( - DebuggingOverlayManager.REACT_CLASS, - ModuleSpec.viewManagerSpec(DebuggingOverlayManager::new)); - mViewManagers = viewManagers; - } - return mViewManagers; - } - - @Override - public List getViewManagers(ReactApplicationContext reactContext) { - return new ArrayList<>(getViewManagersMap().values()); - } - - @Override - public Collection getViewManagerNames(ReactApplicationContext reactContext) { - return getViewManagersMap().keySet(); - } - - @Override - public @Nullable ViewManager createViewManager( - ReactApplicationContext reactContext, String viewManagerName) { - ModuleSpec spec = getViewManagersMap().get(viewManagerName); - return spec != null ? (ViewManager) spec.getProvider().get() : null; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.kt new file mode 100644 index 00000000000000..9e1cd7bc1074bb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react + +import com.facebook.react.bridge.ModuleSpec +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.UIManager +import com.facebook.react.module.annotations.ReactModuleList +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.uimanager.ViewManager +import com.facebook.react.views.debuggingoverlay.DebuggingOverlayManager + +/** + * Package defining core framework modules (e.g. [UIManager]). It should be used for modules that + * require special integration with other framework parts (e.g. with the list of packages to load + * view managers from). + */ +@ReactModuleList(nativeModules = []) +public class DebugCorePackage public constructor() : + BaseReactPackage(), ViewManagerOnDemandReactPackage { + + /** A map of view managers that should be registered with [UIManager] */ + private val viewManagersMap: Map by + lazy(LazyThreadSafetyMode.NONE) { + mapOf( + DebuggingOverlayManager.REACT_CLASS to + ModuleSpec.viewManagerSpec { DebuggingOverlayManager() }) + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { + emptyMap() + } + + public override fun getModule( + name: String, + reactContext: ReactApplicationContext + ): NativeModule? = null + + public override fun getViewManagers(reactContext: ReactApplicationContext): List = + viewManagersMap.values.toList() + + override fun getViewManagerNames(reactContext: ReactApplicationContext): Collection = + viewManagersMap.keys + + override fun createViewManager( + reactContext: ReactApplicationContext, + viewManagerName: String + ): ViewManager<*, *>? = + viewManagersMap.getOrDefault(viewManagerName, null)?.provider?.get() as? ViewManager<*, *> +} 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 5c1d66da87fafe..d9efcc5f144d04 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 @@ -374,7 +374,7 @@ public void destroyRootView(View rootView) { } @Override - public void reload(String s) { + public void reload(String reason) { // no-op not implemented for Bridge Mode } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.java deleted file mode 100644 index fadd6a3e4dd6ef..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.java +++ /dev/null @@ -1,138 +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.devsupport; - -import android.content.Context; -import androidx.annotation.Nullable; -import com.facebook.debug.holder.PrinterHolder; -import com.facebook.debug.tags.ReactDebugOverlayTags; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.JSBundleLoader; -import com.facebook.react.bridge.ReactMarker; -import com.facebook.react.bridge.ReactMarkerConstants; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.SurfaceDelegateFactory; -import com.facebook.react.common.annotations.internal.LegacyArchitecture; -import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel; -import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger; -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; - -/** - * Interface for accessing and interacting with development features. Following features - * are supported through this manager class: - * 1) Displaying JS errors (aka RedBox) - * 2) Displaying developers menu (Reload JS, Debug JS) - * 3) Communication with developer server in order to download updated JS bundle - * 4) Starting/stopping broadcast receiver for js reload signals - * 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may - * trigger developers menu. - * 6) Launching developers settings view - * - * This class automatically monitors the state of registered views and activities to which they are - * bound to make sure that we don't display overlay or that we we don't listen for sensor events - * when app is backgrounded. - * - * {@link com.facebook.react.ReactInstanceManager} implementation is responsible for instantiating - * this class as well as for populating with a reference to {@link CatalystInstance} whenever - * instance manager recreates it (through {@link #onNewReactContextCreated). Also, instance manager - * is responsible for enabling/disabling dev support in case when app is backgrounded or when all - * the views has been detached from the instance (through {@link #setDevSupportEnabled} method). - */ -@LegacyArchitecture -public final class BridgeDevSupportManager extends DevSupportManagerBase { - - static { - LegacyArchitectureLogger.assertLegacyArchitecture( - "BridgeDevSupportManager", LegacyArchitectureLogLevel.WARNING); - } - - private boolean mIsSamplingProfilerEnabled = false; - - public BridgeDevSupportManager( - 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 "Bridge"; - } - - @Override - public void loadSplitBundleFromServer( - final String bundlePath, final DevSplitBundleCallback callback) { - fetchSplitBundleAndCreateBundleLoader( - bundlePath, - new CallbackWithBundleLoader() { - @Override - public void onSuccess(JSBundleLoader bundleLoader) { - bundleLoader.loadScript(getCurrentReactContext().getCatalystInstance()); - getCurrentReactContext() - .getJSModule(HMRClient.class) - .registerBundle(getDevServerHelper().getDevServerSplitBundleURL(bundlePath)); - callback.onSuccess(); - } - - @Override - public void onError(String url, Throwable cause) { - callback.onError(url, cause); - } - }); - } - - @Override - public void handleReloadJS() { - - UiThreadUtil.assertOnUiThread(); - - ReactMarker.logMarker( - ReactMarkerConstants.RELOAD, - getDevSettings().getPackagerConnectionSettings().getDebugServerHost()); - - // dismiss redbox if exists - hideRedboxDialog(); - - PrinterHolder.getPrinter() - .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server"); - String bundleURL = - getDevServerHelper().getDevServerBundleURL(Assertions.assertNotNull(getJSAppBundleName())); - reloadJSFromServer( - bundleURL, - () -> - UiThreadUtil.runOnUiThread( - () -> getReactInstanceDevHelper().onJSBundleLoadedFromServer())); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt new file mode 100644 index 00000000000000..e24c4fd2c69ba2 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt @@ -0,0 +1,119 @@ +/* + * 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 com.facebook.debug.holder.PrinterHolder.printer +import com.facebook.debug.tags.ReactDebugOverlayTags +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.JSBundleLoader +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.ReactMarkerConstants +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.SurfaceDelegateFactory +import com.facebook.react.common.annotations.internal.LegacyArchitecture +import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel +import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger.assertLegacyArchitecture +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 + +/** + * Interface for accessing and interacting with development features. Following features are + * supported through this manager class: + * 1) Displaying JS errors (aka RedBox) + * 2) Displaying developers menu (Reload JS, Debug JS) + * 3) Communication with developer server in order to download updated JS bundle + * 4) Starting/stopping broadcast receiver for js reload signals + * 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may + * trigger developers menu. + * 6) Launching developers settings view + * + * This class automatically monitors the state of registered views and activities to which they are + * bound to make sure that we don't display overlay or that we we don't listen for sensor events + * when app is backgrounded. + * + * [com.facebook.react.ReactInstanceManager] implementation is responsible for instantiating this + * class as well as for populating with a reference to [com.facebook.react.bridge.CatalystInstance] + * whenever instance manager recreates it (through [onNewReactContextCreated]). Also, instance + * manager is responsible for enabling/disabling dev support in case when app is backgrounded or + * when all the views has been detached from the instance (through `setDevSupportEnabled` method). + */ +@LegacyArchitecture +public class BridgeDevSupportManager( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: Map?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? +) : + DevSupportManagerBase( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers, + surfaceDelegateFactory, + devLoadingViewManager, + pausedInDebuggerOverlayManager) { + + override fun getUniqueTag(): String = "Bridge" + + override fun loadSplitBundleFromServer(bundlePath: String, callback: DevSplitBundleCallback) { + fetchSplitBundleAndCreateBundleLoader( + bundlePath, + object : CallbackWithBundleLoader { + override fun onSuccess(bundleLoader: JSBundleLoader) { + val context = + requireNotNull(currentReactContext) { + "Failed to load split bundle from server due to DevSupportManager.currentReactContext being null" + } + bundleLoader.loadScript(context.catalystInstance) + context + .getJSModule(HMRClient::class.java) + .registerBundle(devServerHelper.getDevServerSplitBundleURL(bundlePath)) + callback.onSuccess() + } + + override fun onError(url: String, cause: Throwable) = callback.onError(url, cause) + }) + } + + override fun handleReloadJS() { + UiThreadUtil.assertOnUiThread() + ReactMarker.logMarker( + ReactMarkerConstants.RELOAD, devSettings.packagerConnectionSettings.debugServerHost) + + // dismiss redbox if exists + hideRedboxDialog() + + printer.logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server") + val bundleURL = devServerHelper.getDevServerBundleURL(Assertions.assertNotNull(jsAppBundleName)) + reloadJSFromServer(bundleURL) { + UiThreadUtil.runOnUiThread { reactInstanceDevHelper.onJSBundleLoadedFromServer() } + } + } + + private companion object { + init { + assertLegacyArchitecture("BridgeDevSupportManager", LegacyArchitectureLogLevel.WARNING) + } + } +} 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 deleted file mode 100644 index 355a88d1a0dda5..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.java +++ /dev/null @@ -1,124 +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.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) { - Thread.currentThread().interrupt(); - 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/BridgelessDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt new file mode 100644 index 00000000000000..fd747969635672 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt @@ -0,0 +1,104 @@ +/* + * 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 com.facebook.react.bridge.JSBundleLoader +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.DevSupportManager +import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager +import com.facebook.react.devsupport.interfaces.RedBoxHandler +import com.facebook.react.packagerconnection.RequestHandler + +/** + * An implementation of [DevSupportManager] that extends the functionality in + * [DevSupportManagerBase] with some additional, more flexible APIs for asynchronously loading the + * JS bundle. + * + * @constructor The primary constructor mirrors the same constructor we have for + * [BridgeDevSupportManager] and + * * is kept for backward compatibility. + */ +internal class BridgelessDevSupportManager( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: Map?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? +) : + DevSupportManagerBase( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers, + surfaceDelegateFactory, + devLoadingViewManager, + pausedInDebuggerOverlayManager) { + + constructor( + context: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String? + ) : this( + applicationContext = context.applicationContext, + reactInstanceManagerHelper = reactInstanceManagerHelper, + packagerPathForJSBundleName = packagerPathForJSBundleName, + enableOnCreate = true, + redBoxHandler = null, + devBundleDownloadListener = null, + minNumShakes = 2, + customPackagerCommandHandlers = null, + surfaceDelegateFactory = null, + devLoadingViewManager = null, + pausedInDebuggerOverlayManager = null) + + override fun getUniqueTag(): String = "Bridgeless" + + override fun loadSplitBundleFromServer(bundlePath: String, callback: DevSplitBundleCallback) { + fetchSplitBundleAndCreateBundleLoader( + bundlePath, + object : CallbackWithBundleLoader { + override fun onSuccess(bundleLoader: JSBundleLoader) { + try { + mReactInstanceDevHelper.loadBundle(bundleLoader).waitForCompletion() + val bundleURL = devServerHelper.getDevServerSplitBundleURL(bundlePath) + val reactContext = mReactInstanceDevHelper.currentReactContext + reactContext?.getJSModule(HMRClient::class.java)?.registerBundle(bundleURL) + callback.onSuccess() + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + throw RuntimeException( + "[BridgelessDevSupportManager]: Got interrupted while loading bundle", e) + } + } + + override fun onError(url: String, cause: Throwable) = callback.onError(url, cause) + }) + } + + override fun 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/DebugOverlayController.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java deleted file mode 100644 index 9db6ef475fb916..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java +++ /dev/null @@ -1,120 +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.devsupport; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.graphics.PixelFormat; -import android.net.Uri; -import android.provider.Settings; -import android.view.WindowManager; -import android.widget.FrameLayout; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.ReactConstants; - -/** - * Helper class for controlling overlay view with FPS and JS FPS info that gets added directly - * to @{link WindowManager} instance. - */ -/* package */ @Nullsafe(Nullsafe.Mode.LOCAL) -class DebugOverlayController { - - public static void requestPermission(Context context) { - // Get permission to show debug overlay in dev builds. - if (!Settings.canDrawOverlays(context)) { - Intent intent = - new Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:" + context.getPackageName())); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - FLog.w( - ReactConstants.TAG, - "Overlay permissions needs to be granted in order for react native apps to run in dev" - + " mode"); - if (canHandleIntent(context, intent)) { - context.startActivity(intent); - } - } - } - - private static boolean permissionCheck(Context context) { - // Get permission to show debug overlay in dev builds. - // overlay permission not yet granted - return Settings.canDrawOverlays(context); - } - - private static boolean hasPermission(Context context, String permission) { - try { - PackageInfo info = - context - .getPackageManager() - // NULLSAFE_FIXME[Nullable Dereference] - .getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); - if (info.requestedPermissions != null) { - for (String p : info.requestedPermissions) { - if (p.equals(permission)) { - return true; - } - } - } - } catch (PackageManager.NameNotFoundException e) { - FLog.e(ReactConstants.TAG, "Error while retrieving package info", e); - } - return false; - } - - private static boolean canHandleIntent(Context context, Intent intent) { - PackageManager packageManager = context.getPackageManager(); - return packageManager != null && intent.resolveActivity(packageManager) != null; - } - - private final WindowManager mWindowManager; - private final ReactContext mReactContext; - - private @Nullable FrameLayout mFPSDebugViewContainer; - - public DebugOverlayController(ReactContext reactContext) { - mReactContext = reactContext; - mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE); - } - - public void setFpsDebugViewVisible(final boolean fpsDebugViewVisible) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (fpsDebugViewVisible && mFPSDebugViewContainer == null) { - if (!permissionCheck(mReactContext)) { - FLog.d(ReactConstants.TAG, "Wait for overlay permission to be set"); - return; - } - mFPSDebugViewContainer = new FpsView(mReactContext); - WindowManager.LayoutParams params = - new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT, - WindowOverlayCompat.TYPE_SYSTEM_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - PixelFormat.TRANSLUCENT); - mWindowManager.addView(mFPSDebugViewContainer, params); - } else if (!fpsDebugViewVisible && mFPSDebugViewContainer != null) { - mFPSDebugViewContainer.removeAllViews(); - mWindowManager.removeView(mFPSDebugViewContainer); - mFPSDebugViewContainer = null; - } - } - }); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.kt new file mode 100644 index 00000000000000..3467d9af749473 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.kt @@ -0,0 +1,87 @@ +/* + * 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 android.content.Intent +import android.graphics.PixelFormat +import android.net.Uri +import android.provider.Settings +import android.view.WindowManager +import android.widget.FrameLayout +import com.facebook.common.logging.FLog +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.ReactConstants + +/** + * Helper class for controlling overlay view with FPS and JS FPS info that gets added directly to + * [WindowManager] instance. + */ +internal class DebugOverlayController(private val reactContext: ReactContext) { + private val windowManager = reactContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager + + private var fpsDebugViewContainer: FrameLayout? = null + + fun setFpsDebugViewVisible(fpsDebugViewVisible: Boolean) { + UiThreadUtil.runOnUiThread( + Runnable { + if (fpsDebugViewVisible && fpsDebugViewContainer == null) { + if (!permissionCheck(reactContext)) { + FLog.d(ReactConstants.TAG, "Wait for overlay permission to be set") + return@Runnable + } + fpsDebugViewContainer = FpsView(reactContext) + val params = + WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + WindowOverlayCompat.TYPE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT) + windowManager.addView(fpsDebugViewContainer, params) + } else if (!fpsDebugViewVisible && fpsDebugViewContainer != null) { + fpsDebugViewContainer?.removeAllViews() + windowManager.removeView(fpsDebugViewContainer) + fpsDebugViewContainer = null + } + }) + } + + companion object { + @JvmStatic + fun requestPermission(context: Context) { + // Get permission to show debug overlay in dev builds. + if (!Settings.canDrawOverlays(context)) { + val intent = + Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + context.packageName)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + FLog.w( + ReactConstants.TAG, + "Overlay permissions needs to be granted in order for react native apps to run in dev mode") + if (canHandleIntent(context, intent)) { + context.startActivity(intent) + } + } + } + + private fun permissionCheck(context: Context): Boolean { + // Get permission to show debug overlay in dev builds. + // overlay permission not yet granted + return Settings.canDrawOverlays(context) + } + + private fun canHandleIntent(context: Context, intent: Intent): Boolean { + val packageManager = context.packageManager + return packageManager != null && intent.resolveActivity(packageManager) != null + } + } +} 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 df5724d8083e34..bb03454ac27c71 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 @@ -95,7 +95,7 @@ internal class DefaultDevSupportManagerFactory : DevSupportManagerFactory { redBoxHandler: RedBoxHandler?, devBundleDownloadListener: DevBundleDownloadListener?, minNumShakes: Int, - customPackagerCommandHandlers: MutableMap?, + customPackagerCommandHandlers: Map?, surfaceDelegateFactory: SurfaceDelegateFactory?, devLoadingViewManager: DevLoadingViewManager?, pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?, 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 0d68f1a2d255c0..734ad9b1281d83 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 @@ -38,6 +38,7 @@ import com.facebook.react.R; import com.facebook.react.bridge.DefaultJSExceptionHandler; import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarkerConstants; @@ -540,7 +541,13 @@ public View getView(int position, @Nullable View convertView, ViewGroup parent) private @Nullable String getJSExecutorDescription() { try { - return getReactInstanceDevHelper().getJavaScriptExecutorFactory().toString(); + JavaScriptExecutorFactory factory = + getReactInstanceDevHelper().getJavaScriptExecutorFactory(); + if (factory != null) { + return factory.toString(); + } else { + return null; + } } catch (IllegalStateException e) { return null; } 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 deleted file mode 100644 index 29d9f7e3053be9..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java +++ /dev/null @@ -1,56 +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.devsupport; - -import android.content.Context; -import androidx.annotation.Nullable; -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.DevSupportManager; -import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager; -import com.facebook.react.devsupport.interfaces.RedBoxHandler; -import com.facebook.react.packagerconnection.RequestHandler; -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, - @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); - - /** - * 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/DevSupportManagerFactory.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt new file mode 100644 index 00000000000000..c006de2d7ba865 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.kt @@ -0,0 +1,57 @@ +/* + * 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 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.DevSupportManager +import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager +import com.facebook.react.devsupport.interfaces.RedBoxHandler +import com.facebook.react.packagerconnection.RequestHandler + +public interface DevSupportManagerFactory { + + /** + * Factory used by the Old Architecture flow to create a [DevSupportManager] and a + * [BridgeDevSupportManager] + */ + public fun create( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: Map?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? + ): DevSupportManager + + /** + * Factory used by the New Architecture/Bridgeless flow to create a [DevSupportManager] and a + * [BridgelessDevSupportManager] + */ + public fun create( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: Map?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?, + useDevSupport: Boolean + ): DevSupportManager +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.java deleted file mode 100644 index 4c99f9bee75fbb..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.java +++ /dev/null @@ -1,170 +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.devsupport; - -import com.facebook.infer.annotation.Nullsafe; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import okio.Buffer; -import okio.BufferedSource; -import okio.ByteString; - -/** Utility class to parse the body of a response of type multipart/mixed. */ -@Nullsafe(Nullsafe.Mode.LOCAL) -class MultipartStreamReader { - // Standard line separator for HTTP. - private static final String CRLF = "\r\n"; - - private final BufferedSource mSource; - private final String mBoundary; - private long mLastProgressEvent; - - public interface ChunkListener { - /** Invoked when a chunk of a multipart response is fully downloaded. */ - void onChunkComplete(Map headers, Buffer body, boolean isLastChunk) - throws IOException; - - /** Invoked as bytes of the current chunk are read. */ - void onChunkProgress(Map headers, long loaded, long total) throws IOException; - } - - public MultipartStreamReader(BufferedSource source, String boundary) { - mSource = source; - mBoundary = boundary; - } - - private Map parseHeaders(Buffer data) { - Map headers = new HashMap<>(); - - String text = data.readUtf8(); - String[] lines = text.split(CRLF); - for (String line : lines) { - int indexOfSeparator = line.indexOf(":"); - if (indexOfSeparator == -1) { - continue; - } - - String key = line.substring(0, indexOfSeparator).trim(); - String value = line.substring(indexOfSeparator + 1).trim(); - headers.put(key, value); - } - - return headers; - } - - private void emitChunk(Buffer chunk, boolean done, ChunkListener listener) throws IOException { - ByteString marker = ByteString.encodeUtf8(CRLF + CRLF); - long indexOfMarker = chunk.indexOf(marker); - if (indexOfMarker == -1) { - listener.onChunkComplete(Collections.emptyMap(), chunk, done); - } else { - Buffer headers = new Buffer(); - Buffer body = new Buffer(); - chunk.read(headers, indexOfMarker); - chunk.skip(marker.size()); - chunk.readAll(body); - listener.onChunkComplete(parseHeaders(headers), body, done); - } - } - - private void emitProgress( - Map headers, long contentLength, boolean isFinal, ChunkListener listener) - throws IOException { - if (headers == null || listener == null) { - return; - } - - long currentTime = System.currentTimeMillis(); - if (currentTime - mLastProgressEvent > 16 || isFinal) { - mLastProgressEvent = currentTime; - long headersContentLength = - headers.get("Content-Length") != null ? Long.parseLong(headers.get("Content-Length")) : 0; - listener.onChunkProgress(headers, contentLength, headersContentLength); - } - } - - /** - * Reads all parts of the multipart response and execute the listener for each chunk received. - * - * @param listener Listener invoked when chunks are received. - * @return If the read was successful - */ - public boolean readAllParts(ChunkListener listener) throws IOException { - ByteString delimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + CRLF); - ByteString closeDelimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + "--" + CRLF); - ByteString headersDelimiter = ByteString.encodeUtf8(CRLF + CRLF); - - int bufferLen = 4 * 1024; - long chunkStart = 0; - long bytesSeen = 0; - Buffer content = new Buffer(); - Map currentHeaders = null; - long currentHeadersLength = 0; - - while (true) { - boolean isCloseDelimiter = false; - - // Search only a subset of chunk that we haven't seen before + few bytes - // to allow for the edge case when the delimiter is cut by read call. - long searchStart = Math.max(bytesSeen - closeDelimiter.size(), chunkStart); - long indexOfDelimiter = content.indexOf(delimiter, searchStart); - if (indexOfDelimiter == -1) { - isCloseDelimiter = true; - indexOfDelimiter = content.indexOf(closeDelimiter, searchStart); - } - - if (indexOfDelimiter == -1) { - bytesSeen = content.size(); - - if (currentHeaders == null) { - long indexOfHeaders = content.indexOf(headersDelimiter, searchStart); - if (indexOfHeaders >= 0) { - mSource.read(content, indexOfHeaders); - Buffer headers = new Buffer(); - content.copyTo(headers, searchStart, indexOfHeaders - searchStart); - currentHeadersLength = headers.size() + headersDelimiter.size(); - currentHeaders = parseHeaders(headers); - } - } else { - emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener); - } - - long bytesRead = mSource.read(content, bufferLen); - if (bytesRead <= 0) { - return false; - } - continue; - } - - long chunkEnd = indexOfDelimiter; - long length = chunkEnd - chunkStart; - - // Ignore preamble - if (chunkStart > 0) { - Buffer chunk = new Buffer(); - content.skip(chunkStart); - content.read(chunk, length); - // NULLSAFE_FIXME[Parameter Not Nullable] - emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener); - emitChunk(chunk, isCloseDelimiter, listener); - currentHeaders = null; - currentHeadersLength = 0; - } else { - content.skip(chunkEnd); - } - - if (isCloseDelimiter) { - return true; - } - - bytesSeen = chunkStart = delimiter.size(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt new file mode 100644 index 00000000000000..502e68c629189b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt @@ -0,0 +1,168 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION_ERROR") // Conflicting okio versions + +package com.facebook.react.devsupport + +import java.io.IOException +import kotlin.math.max +import okio.Buffer +import okio.BufferedSource +import okio.ByteString + +/** Utility class to parse the body of a response of type multipart/mixed. */ +internal class MultipartStreamReader( + private val source: BufferedSource, + private val boundary: String +) { + private var lastProgressEvent: Long = 0 + + interface ChunkListener { + /** Invoked when a chunk of a multipart response is fully downloaded. */ + @Throws(IOException::class) + fun onChunkComplete(headers: Map, body: Buffer?, isLastChunk: Boolean) + + /** Invoked as bytes of the current chunk are read. */ + @Throws(IOException::class) + fun onChunkProgress(headers: Map, loaded: Long, total: Long) + } + + /** + * Reads all parts of the multipart response and execute the listener for each chunk received. + * + * @param listener Listener invoked when chunks are received. + * @return If the read was successful + */ + @Throws(IOException::class) + fun readAllParts(listener: ChunkListener): Boolean { + val delimiter: ByteString = ByteString.encodeUtf8("$CRLF--$boundary$CRLF") + val closeDelimiter: ByteString = ByteString.encodeUtf8("$CRLF--$boundary--$CRLF") + val headersDelimiter: ByteString = ByteString.encodeUtf8(CRLF + CRLF) + + val bufferLen = 4 * 1024 + var chunkStart: Long = 0 + var bytesSeen: Long = 0 + val content = Buffer() + var currentHeaders: Map? = null + var currentHeadersLength: Long = 0 + + while (true) { + var isCloseDelimiter = false + + // Search only a subset of chunk that we haven't seen before + few bytes + // to allow for the edge case when the delimiter is cut by read call. + val searchStart = + max((bytesSeen - closeDelimiter.size()).toDouble(), chunkStart.toDouble()).toLong() + var indexOfDelimiter = content.indexOf(delimiter, searchStart) + if (indexOfDelimiter == -1L) { + isCloseDelimiter = true + indexOfDelimiter = content.indexOf(closeDelimiter, searchStart) + } + + if (indexOfDelimiter == -1L) { + bytesSeen = content.size() + + if (currentHeaders == null) { + val indexOfHeaders = content.indexOf(headersDelimiter, searchStart) + if (indexOfHeaders >= 0) { + source.read(content, indexOfHeaders) + val headers = Buffer() + content.copyTo(headers, searchStart, indexOfHeaders - searchStart) + currentHeadersLength = headers.size() + headersDelimiter.size() + currentHeaders = parseHeaders(headers) + } + } else { + emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener) + } + + val bytesRead = source.read(content, bufferLen.toLong()) + if (bytesRead <= 0) { + return false + } + continue + } + + val chunkEnd = indexOfDelimiter + val length = chunkEnd - chunkStart + + // Ignore preamble + if (chunkStart > 0) { + val chunk = Buffer() + content.skip(chunkStart) + content.read(chunk, length) + // NULLSAFE_FIXME[Parameter Not Nullable] + emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener) + emitChunk(chunk, isCloseDelimiter, listener) + currentHeaders = null + currentHeadersLength = 0 + } else { + content.skip(chunkEnd) + } + if (isCloseDelimiter) { + return true + } + chunkStart = delimiter.size().toLong() + bytesSeen = chunkStart + } + } + + private fun parseHeaders(data: Buffer): Map { + val headers: MutableMap = mutableMapOf() + val text = data.readUtf8() + val lines = text.split(CRLF.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (line in lines) { + val indexOfSeparator = line.indexOf(":") + if (indexOfSeparator == -1) { + continue + } + val key = line.substring(0, indexOfSeparator).trim { it <= ' ' } + val value = line.substring(indexOfSeparator + 1).trim { it <= ' ' } + headers[key] = value + } + return headers + } + + @Throws(IOException::class) + private fun emitChunk(chunk: Buffer, done: Boolean, listener: ChunkListener) { + val marker: ByteString = ByteString.encodeUtf8(CRLF + CRLF) + val indexOfMarker = chunk.indexOf(marker) + if (indexOfMarker == -1L) { + listener.onChunkComplete(emptyMap(), chunk, done) + } else { + val headers = Buffer() + val body = Buffer() + chunk.read(headers, indexOfMarker) + chunk.skip(marker.size().toLong()) + chunk.readAll(body) + listener.onChunkComplete(parseHeaders(headers), body, done) + } + } + + @Throws(IOException::class) + private fun emitProgress( + headers: Map?, + contentLength: Long, + isFinal: Boolean, + listener: ChunkListener? + ) { + if (listener == null || headers == null) { + return + } + val currentTime = System.currentTimeMillis() + if (currentTime - lastProgressEvent > 16 || isFinal) { + lastProgressEvent = currentTime + val headersContentLength = headers.getOrDefault("Content-Length", "0").toLong() + listener.onChunkProgress(headers, contentLength, headersContentLength) + } + } + + companion object { + // Standard line separator for HTTP. + private const val CRLF = "\r\n" + } +} 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 deleted file mode 100644 index 918f569425ae33..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.java +++ /dev/null @@ -1,47 +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.devsupport; - -import android.app.Activity; -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.react.bridge.JSBundleLoader; -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 ReactHost} for the purpose of displaying and handling developer - * menu options. - */ -public interface ReactInstanceDevHelper { - /** Notify react instance manager about new JS bundle version downloaded from the server. */ - void onJSBundleLoadedFromServer(); - - /** Request to toggle the react element inspector. */ - void toggleElementInspector(); - - /** Get reference to top level #{link Activity} attached to react context */ - @Nullable - Activity getCurrentActivity(); - - JavaScriptExecutorFactory getJavaScriptExecutorFactory(); - - @Nullable - 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/devsupport/ReactInstanceDevHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.kt new file mode 100644 index 00000000000000..f9632700e41107 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevHelper.kt @@ -0,0 +1,44 @@ +/* + * 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.app.Activity +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.devsupport.interfaces.DevSupportManager +import com.facebook.react.interfaces.TaskInterface + +/** + * Interface used by [DevSupportManager] for accessing some fields and methods of [ReactHost] for + * the purpose of displaying and handling developer menu options. + */ +public interface ReactInstanceDevHelper { + + /** Get reference to top level [Activity] attached to react context */ + public val currentActivity: Activity? + + public val javaScriptExecutorFactory: JavaScriptExecutorFactory + + public val currentReactContext: ReactContext? + + /** Notify react instance manager about new JS bundle version downloaded from the server. */ + public fun onJSBundleLoadedFromServer() + + /** Request to toggle the react element inspector. */ + public fun toggleElementInspector() + + public fun createRootView(appKey: String): View? + + public fun destroyRootView(rootView: View) + + public fun reload(reason: String) + + public fun loadBundle(bundleLoader: JSBundleLoader): TaskInterface +} 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 5af9594ee44b9d..91351a09f89719 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 @@ -26,6 +26,15 @@ import com.facebook.react.modules.core.DeviceEventManagerModule internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : ReactInstanceDevHelper { + override val currentActivity: Activity? + get() = delegate.lastUsedActivity + + override val javaScriptExecutorFactory: JavaScriptExecutorFactory + get() = throw IllegalStateException("Not implemented for bridgeless mode") + + override val currentReactContext: ReactContext? + get() = delegate.currentReactContext + override fun onJSBundleLoadedFromServer() { // Not implemented, only referenced by BridgeDevSupportManager } @@ -37,12 +46,6 @@ internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : ?.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)) { @@ -59,12 +62,10 @@ internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : // Not implemented, only referenced by BridgeDevSupportManager } - override fun reload(s: String) { - delegate.reload(s) + override fun reload(reason: String) { + delegate.reload(reason) } override fun loadBundle(bundleLoader: JSBundleLoader): TaskInterface = delegate.loadBundle(bundleLoader) - - override fun getCurrentReactContext(): ReactContext? = delegate.currentReactContext }