package expo.modules.devlauncher.helpers import android.content.Context import android.net.Uri import android.util.Log import com.facebook.react.ReactHost import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.defaults.DefaultReactHostDelegate import com.facebook.react.devsupport.DevLauncherDevServerHelper import com.facebook.react.devsupport.DevLauncherSettings import com.facebook.react.devsupport.DevServerHelper import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.modules.systeminfo.AndroidInfoHelpers import com.facebook.react.runtime.ReactHostDelegate import com.facebook.react.runtime.ReactHostImpl import expo.modules.devlauncher.launcher.DevLauncherControllerInterface import expo.modules.devlauncher.react.DevLauncherBridgelessDevSupportManager import expo.modules.devlauncher.react.DevLauncherDevSupportManagerSwapper import expo.modules.devmenu.helpers.setPrivateDeclaredFieldValue import okhttp3.HttpUrl // Sync this class name with ExpoReactHostFactory.kt private const val EXPO_REACT_HOST_DELEGATE_CLASS = "expo.modules.ExpoReactHostFactory.ExpoReactHostDelegate" fun injectReactInterceptor( context: Context, reactHost: ReactHost, url: Uri ): Boolean { val (debugServerHost, appBundleName) = parseUrl(url) injectDevSupportManager(reactHost) val result = injectDebugServerHost( context, reactHost, debugServerHost, appBundleName ) (reactHost.devSupportManager as? DevLauncherBridgelessDevSupportManager)?.startInspectorWhenDevLauncherReady() return result } private fun injectDevSupportManager(reactHost: ReactHost) { DevLauncherDevSupportManagerSwapper().swapDevSupportManagerImpl(reactHost) } fun injectDebugServerHost( context: Context, reactHost: ReactHost, debugServerHost: String, appBundleName: String ): Boolean { return try { val devSupportManager = requireNotNull(reactHost.devSupportManager) injectDebugServerHost(context, devSupportManager, debugServerHost, appBundleName) true } catch (e: Exception) { Log.e("DevLauncher", "Unable to inject debug server host settings.", e) false } } private fun injectDebugServerHost( context: Context, devSupportManager: DevSupportManager, debugServerHost: String, appBundleName: String ) { val settings = DevLauncherSettings(context, debugServerHost) val devSupportManagerBaseClass: Class<*> = DevSupportManagerBase::class.java devSupportManagerBaseClass.setProtectedDeclaredField( devSupportManager, "jsAppBundleName", appBundleName ) val mDevSettingsField = devSupportManagerBaseClass.getDeclaredField("devSettings") mDevSettingsField.isAccessible = true mDevSettingsField[devSupportManager] = settings val mDevServerHelperField = devSupportManagerBaseClass.getDeclaredField("devServerHelper") mDevServerHelperField.isAccessible = true val devServerHelper = mDevServerHelperField[devSupportManager] check(devServerHelper is DevLauncherDevServerHelper) val mSettingsField = DevServerHelper::class.java.getDeclaredField("settings") mSettingsField.isAccessible = true mSettingsField[devServerHelper] = settings val packagerConnectionSettingsField = DevServerHelper::class.java.getDeclaredField("packagerConnectionSettings") packagerConnectionSettingsField.isAccessible = true packagerConnectionSettingsField[devServerHelper] = settings.public_getPackagerConnectionSettings() } @OptIn(UnstableReactNativeAPI::class) fun injectBundleLoader( reactHost: ReactHost, jsBundleLoader: JSBundleLoader ): Boolean { return try { check(reactHost is ReactHostImpl) // [0] Disable `mAllowPackagerServerAccess` // so that ReactHost could use jsBundlerLoader from ReactHostDelegate val reactHostClass = ReactHostImpl::class.java val mAllowPackagerServerAccessField = reactHostClass.getDeclaredField("allowPackagerServerAccess") mAllowPackagerServerAccessField.isAccessible = true mAllowPackagerServerAccessField[reactHost] = false // [1] Replace the ReactHostDelegate.jsBundlerLoader with our new loader val mReactHostDelegateField = reactHostClass.getDeclaredField("reactHostDelegate") mReactHostDelegateField.isAccessible = true val reactHostDelegate = mReactHostDelegateField[reactHost] as ReactHostDelegate if (reactHostDelegate.javaClass.canonicalName == EXPO_REACT_HOST_DELEGATE_CLASS) { reactHostDelegate.javaClass.setPrivateDeclaredFieldValue( "_jsBundleLoader", reactHostDelegate, jsBundleLoader ) } else if (reactHostDelegate is DefaultReactHostDelegate) { DefaultReactHostDelegate::class.java.setPrivateDeclaredFieldValue( "jsBundleLoader", reactHostDelegate, jsBundleLoader ) } else { throw IllegalStateException("[injectBundleLoader] Unsupported reactHostDelegate: ${reactHostDelegate.javaClass}") } true } catch (e: Exception) { Log.e("DevLauncher", "Unable to inject bundle loader", e) false } } fun injectLocalBundleLoader( reactHost: ReactHost, bundlePath: String ): Boolean { return injectBundleLoader(reactHost, JSBundleLoader.createFileLoader(bundlePath)) } fun injectDevServerHelper(context: Context, devSupportManager: DevSupportManager, controller: DevLauncherControllerInterface?) { val defaultServerHost = AndroidInfoHelpers.getServerHost(context) val devSettings = DevLauncherSettings(context, defaultServerHost) val devLauncherDevServerHelper = DevLauncherDevServerHelper( context = context, controller = controller, devSettings = devSettings, packagerConnection = devSettings.public_getPackagerConnectionSettings() ) val oldDevServerHelper: DevServerHelper = DevSupportManagerBase::class.java.getProtectedFieldValue( devSupportManager, "devServerHelper" ) DevSupportManagerBase::class.java.setProtectedDeclaredField( devSupportManager, "devServerHelper", devLauncherDevServerHelper ) oldDevServerHelper.closePackagerConnection() oldDevServerHelper.closeInspectorConnection() } private fun parseUrl(url: Uri): Pair { val port = if (url.port != -1) url.port else HttpUrl.defaultPort(url.scheme ?: "http") val debugServerHost = url.host + ":" + port // We need to remove "/" which is added to begin of the path by the Uri // and the bundle type val appBundleName = if (url.path.isNullOrEmpty()) { "index" } else { url.path ?.substring(1) ?.replace(".bundle", "") ?: "index" } return Pair(debugServerHost, appBundleName) }