diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0dbbd9dbd047bd..5f8595ff671e8b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -239,6 +239,9 @@ public abstract interface class com/facebook/react/ReactHost { public abstract fun reload (Ljava/lang/String;)Lcom/facebook/react/interfaces/TaskInterface; public abstract fun removeBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V public abstract fun removeReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V + public fun setBundleSource (Ljava/lang/String;)V + public fun setBundleSource (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun setBundleSource$default (Lcom/facebook/react/ReactHost;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public fun setDevMenuConfiguration (Lcom/facebook/react/devsupport/DevMenuConfiguration;)V public abstract fun start ()Lcom/facebook/react/interfaces/TaskInterface; } @@ -1943,6 +1946,7 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/ public fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File; public final fun fetchSplitBundleAndCreateBundleLoader (Ljava/lang/String;Lcom/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader;)V protected final fun getApplicationContext ()Landroid/content/Context; + public fun getBundleFilePath ()Ljava/lang/String; public fun getCurrentActivity ()Landroid/app/Activity; public final fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext; public final fun getDevLoadingViewManager ()Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager; @@ -1977,11 +1981,13 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/ public fun reloadJSFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/BundleLoadCallback;)V public fun reloadSettings ()V public fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V + public fun setBundleFilePath (Ljava/lang/String;)V public final fun setDevLoadingViewManager (Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;)V public fun setDevMenuEnabled (Z)V public final fun setDevSupportEnabled (Z)V public fun setFpsDebugEnabled (Z)V public fun setHotModuleReplacementEnabled (Z)V + public final fun setJsAppBundleName (Ljava/lang/String;)V public fun setKeyboardShortcutsEnabled (Z)V public final fun setLastErrorCookie (I)V public final fun setLastErrorStack ([Lcom/facebook/react/devsupport/interfaces/StackFrame;)V @@ -2151,6 +2157,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevSupp public abstract fun createSurfaceDelegate (Ljava/lang/String;)Lcom/facebook/react/common/SurfaceDelegate; public abstract fun destroyRootView (Landroid/view/View;)V public abstract fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File; + public fun getBundleFilePath ()Ljava/lang/String; public abstract fun getCurrentActivity ()Landroid/app/Activity; public abstract fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext; public fun getDevMenuEnabled ()Z @@ -2180,6 +2187,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevSupp public abstract fun reloadJSFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/BundleLoadCallback;)V public abstract fun reloadSettings ()V public abstract fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V + public fun setBundleFilePath (Ljava/lang/String;)V public fun setDevMenuEnabled (Z)V public abstract fun setDevSupportEnabled (Z)V public abstract fun setFpsDebugEnabled (Z)V @@ -3021,6 +3029,8 @@ public class com/facebook/react/packagerconnection/PackagerConnectionSettings { public fun resetDebugServerHost ()V public final fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V public fun setDebugServerHost (Ljava/lang/String;)V + public final fun setPackagerOptionsUpdater (Lkotlin/jvm/functions/Function1;)V + public final fun updatePackagerOptions (Ljava/util/Map;)Ljava/util/Map; } public final class com/facebook/react/packagerconnection/ReconnectingWebSocket : okhttp3/WebSocketListener { @@ -3099,6 +3109,8 @@ public final class com/facebook/react/runtime/ReactHostImpl : com/facebook/react public fun reload (Ljava/lang/String;)Lcom/facebook/react/interfaces/TaskInterface; public fun removeBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V public fun removeReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V + public fun setBundleSource (Ljava/lang/String;)V + public fun setBundleSource (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun setDevMenuConfiguration (Lcom/facebook/react/devsupport/DevMenuConfiguration;)V public fun start ()Lcom/facebook/react/interfaces/TaskInterface; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt index 583bc2aed37d52..c0912c4945276a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt @@ -193,4 +193,21 @@ public interface ReactHost { /** Set the DevMenu configuration. */ public fun setDevMenuConfiguration(config: DevMenuConfiguration): Unit = Unit + + /** Sets the source of the bundle to be loaded from the file system. */ + public fun setBundleSource(filePath: String): Unit = Unit + + /** + * Sets the source of the bundle to be loaded from the packager server and updates the packager + * connection. + * + * @param debugServerHost host and port of the server, for example "localhost:8081" + * @param moduleName the module name to load, for example "js/RNTesterApp.android" + * @param queryMapper a function that takes current packager options and returns updated options + */ + public fun setBundleSource( + debugServerHost: String, + moduleName: String, + queryMapper: (Map) -> Map = { it }, + ): Unit = Unit } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.kt index d7777c3b8f7d06..cfbceaac4daeeb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.kt @@ -280,7 +280,11 @@ public open class DevServerHelper( ): String { val dev = devMode val additionalOptionsBuilder = StringBuilder() - for ((key, value) in packagerConnectionSettings.additionalOptionsForPackager) { + val packagerOptions = + packagerConnectionSettings.updatePackagerOptions( + packagerConnectionSettings.additionalOptionsForPackager + ) + for ((key, value) in packagerOptions) { if (value.isEmpty()) { continue } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt index 95d9307d6c3efc..40cdf48cb33ac4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt @@ -85,7 +85,7 @@ import java.util.Locale public abstract class DevSupportManagerBase( protected val applicationContext: Context, public val reactInstanceDevHelper: ReactInstanceDevHelper, - @get:JvmName("getJSAppBundleName") public val jsAppBundleName: String?, + @get:JvmName("getJSAppBundleName") public var jsAppBundleName: String?, enableOnCreate: Boolean, public override val redBoxHandler: RedBoxHandler?, private val devBundleDownloadListener: DevBundleDownloadListener?, @@ -148,6 +148,12 @@ public abstract class DevSupportManagerBase( field = value } + override var bundleFilePath: String? = null + get() = field + set(value) { + field = value + } + override val sourceMapUrl: String get() = jsAppBundleName?.let { devServerHelper.getSourceMapUrl(it) } ?: "" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.kt index fef972a17205c5..f48122aeb9e302 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.kt @@ -48,6 +48,10 @@ public interface DevSupportManager : JSExceptionHandler { get() = true set(value) = Unit + public var bundleFilePath: String? + get() = null + set(value) = Unit + public var devSupportEnabled: Boolean public fun showNewJavaError(message: String?, e: Throwable) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/PackagerConnectionSettings.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/PackagerConnectionSettings.kt index 6ac6731e07376c..06405c0ab13f29 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/PackagerConnectionSettings.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/PackagerConnectionSettings.kt @@ -4,30 +4,24 @@ * 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") // PreferenceManager should be migrated to androidx - package com.facebook.react.packagerconnection import android.content.Context -import android.content.SharedPreferences -import android.preference.PreferenceManager import com.facebook.common.logging.FLog import com.facebook.react.modules.systeminfo.AndroidInfoHelpers public open class PackagerConnectionSettings(private val appContext: Context) { - private val preferences: SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(appContext) public val packageName: String = appContext.packageName private val _additionalOptionsForPackager: MutableMap = mutableMapOf() + private var _packagerOptionsUpdater: (Map) -> Map = { it } + private var cachedHost: String? = null public open var debugServerHost: String get() { - // Check host setting first. If empty try to detect emulator type and use default + // Check cached host first. If empty try to detect emulator type and use default // hostname for those - val hostFromSettings = preferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null) - if (!hostFromSettings.isNullOrEmpty()) { - return hostFromSettings + cachedHost?.let { + return it } val host = AndroidInfoHelpers.getServerHost(appContext) if (host == AndroidInfoHelpers.DEVICE_LOCALHOST) { @@ -36,20 +30,29 @@ public open class PackagerConnectionSettings(private val appContext: Context) { "You seem to be running on device. Run '${AndroidInfoHelpers.getAdbReverseTcpCommand(appContext)}' to forward the debug server's port to the device.", ) } + + cachedHost = host return host } set(host) { if (host.isEmpty()) { - preferences.edit().remove(PREFS_DEBUG_SERVER_HOST_KEY).apply() + cachedHost = null } else { - preferences.edit().putString(PREFS_DEBUG_SERVER_HOST_KEY, host).apply() + cachedHost = host } } public open fun resetDebugServerHost() { - preferences.edit().remove(PREFS_DEBUG_SERVER_HOST_KEY).apply() + cachedHost = null } + public fun setPackagerOptionsUpdater(queryMapper: (Map) -> Map) { + _packagerOptionsUpdater = queryMapper + } + + public fun updatePackagerOptions(options: Map): Map = + _packagerOptionsUpdater(options) + public fun setAdditionalOptionForPackager(key: String, value: String) { _additionalOptionsForPackager[key] = value } @@ -59,6 +62,5 @@ public open class PackagerConnectionSettings(private val appContext: Context) { private companion object { private val TAG = PackagerConnectionSettings::class.java.simpleName - private const val PREFS_DEBUG_SERVER_HOST_KEY = "debug_http_host" } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 954cfb15fc3414..473fa23babb102 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -74,6 +74,9 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import kotlin.Unit import kotlin.concurrent.Volatile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch /** * A ReactHost is an object that manages a single [ReactInstance]. A ReactHost can be constructed @@ -650,6 +653,29 @@ public class ReactHostImpl( } } + @ThreadConfined(value = ThreadConfined.UI) + override fun setBundleSource(filePath: String) { + devSupportManager.bundleFilePath = filePath + reload("Change bundle source") + } + + @ThreadConfined(value = ThreadConfined.UI) + override fun setBundleSource( + debugServerHost: String, + moduleName: String, + queryMapper: (Map) -> Map, + ) { + CoroutineScope(Dispatchers.Default).launch { + (devSupportManager as DevSupportManagerBase).devServerHelper.closePackagerConnection() + devSupportManager.devSettings.packagerConnectionSettings.let { it -> + it.setPackagerOptionsUpdater(queryMapper) + it.debugServerHost = debugServerHost + } + devSupportManager.jsAppBundleName = moduleName + reload("Changed bundle source") + } + } + @ThreadConfined(ThreadConfined.UI) override fun onConfigurationChanged(context: Context) { val currentReactContext = this.currentReactContext @@ -1064,6 +1090,16 @@ public class ReactHostImpl( get() { stateTracker.enterState("getJSBundleLoader()") + if (devSupportManager.bundleFilePath != null) { + return try { + Task.forResult( + JSBundleLoader.createFileLoader(checkNotNull(devSupportManager.bundleFilePath)) + ) + } catch (e: Exception) { + Task.forError(e) + } + } + if (useDevSupport && allowPackagerServerAccess) { return isMetroRunning.onSuccessTask( { task ->