package expo.modules.devlauncher.react import android.util.Log import com.facebook.react.ReactHost import com.facebook.react.ReactInstanceManager import com.facebook.react.ReactNativeHost import com.facebook.react.common.ShakeDetector import com.facebook.react.devsupport.DevServerHelper import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.devsupport.ReleaseDevSupportManager import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.packagerconnection.JSPackagerClient import com.facebook.react.runtime.ReactHostImpl import expo.interfaces.devmenu.ReactHostWrapper import expo.modules.devlauncher.helpers.getProtectedFieldValue import expo.modules.devlauncher.helpers.setProtectedDeclaredField import expo.modules.devlauncher.koin.DevLauncherKoinComponent import expo.modules.devlauncher.launcher.DevLauncherControllerInterface import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.core.component.inject internal class DevLauncherDevSupportManagerSwapper : DevLauncherKoinComponent { private val controller: DevLauncherControllerInterface by inject() fun swapDevSupportManagerImpl(reactHost: ReactHostWrapper) { if (reactHost.isBridgelessMode) { swapDevSupportManagerImpl(reactHost.reactHost) } else { swapDevSupportManagerImpl(reactHost.reactNativeHost) } } private fun swapDevSupportManagerImpl(reactNativeHost: ReactNativeHost) { val reactInstanceManager = reactNativeHost.reactInstanceManager val currentDevSupportManager = reactInstanceManager.devSupportManager if (currentDevSupportManager is DevLauncherBridgeDevSupportManager) { // DevSupportManager was swapped by the DevLauncherReactNativeHostHandler return } if (currentDevSupportManager is ReleaseDevSupportManager) { Log.i("DevLauncher", "DevSupportManager is disabled. So we don't want to override it.") return } try { val devManagerClass = DevSupportManagerBase::class.java val newDevSupportManager = createDevLauncherBridgeDevSupportManager(devManagerClass, currentDevSupportManager) ReactInstanceManager::class.java.setProtectedDeclaredField(reactInstanceManager, "mDevSupportManager", newDevSupportManager) closeExistingConnection(devManagerClass, currentDevSupportManager) } catch (e: Exception) { Log.i("DevLauncher", "Couldn't inject `DevLauncherDevSupportManager`.", e) } } private fun swapDevSupportManagerImpl(reactHost: ReactHost) { val currentDevSupportManager = requireNotNull(reactHost.devSupportManager) if (currentDevSupportManager is DevLauncherBridgelessDevSupportManager) { // DevSupportManager was swapped by the DevLauncherReactNativeHostHandler return } if (currentDevSupportManager is ReleaseDevSupportManager) { Log.i("DevLauncher", "DevSupportManager is disabled. So we don't want to override it.") return } try { val devManagerClass = DevSupportManagerBase::class.java val newDevSupportManager = createDevLauncherBridgelessDevSupportManager( devManagerClass, currentDevSupportManager ) ReactHostImpl::class.java.setProtectedDeclaredField(reactHost, "mDevSupportManager", newDevSupportManager) closeExistingConnection(devManagerClass, currentDevSupportManager) } catch (e: Exception) { Log.i("DevLauncher", "Couldn't inject `DevLauncherDevSupportManager`.", e) } } private fun createDevLauncherBridgeDevSupportManager( devManagerClass: Class<*>, currentDevSupportManager: DevSupportManager ): DevLauncherBridgeDevSupportManager { return DevLauncherBridgeDevSupportManager( applicationContext = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mApplicationContext"), reactInstanceDevHelper = devManagerClass.getProtectedFieldValue(currentDevSupportManager, DevLauncherBridgeDevSupportManager.getDevHelperInternalFieldName()), packagerPathForJSBundleName = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mJSAppBundleName"), enableOnCreate = true, redBoxHandler = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mRedBoxHandler"), devBundleDownloadListener = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mBundleDownloadListener"), minNumShakes = 1, customPackagerCommandHandlers = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mCustomPackagerCommandHandlers") ) } private fun createDevLauncherBridgelessDevSupportManager( devManagerClass: Class<*>, currentDevSupportManager: DevSupportManager ): DevLauncherBridgelessDevSupportManager { return DevLauncherBridgelessDevSupportManager( applicationContext = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mApplicationContext"), reactInstanceDevHelper = devManagerClass.getProtectedFieldValue(currentDevSupportManager, DevLauncherBridgeDevSupportManager.getDevHelperInternalFieldName()), packagerPathForJSBundleName = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mJSAppBundleName"), enableOnCreate = true, redBoxHandler = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mRedBoxHandler"), devBundleDownloadListener = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mBundleDownloadListener"), minNumShakes = 1, customPackagerCommandHandlers = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mCustomPackagerCommandHandlers") ) } /** * We need to invalidate the old packager connection. * However, this connection is established in the background * and we don't know when it will be available (see [DevServerHelper.openPackagerConnection]). * So we just wait for connection and then we kill it. */ private fun closeExistingConnection(devManagerClass: Class<*>, devSupportManager: DevSupportManager) { controller.coroutineScope.launch { try { while (true) { // Invalidate shake detector - not doing that leads to memory leaks tryToStopShakeDetector(devSupportManager) val devServerHelper: DevServerHelper = devManagerClass.getProtectedFieldValue( devSupportManager, "mDevServerHelper" ) try { val packagerConnectionLock: Boolean = DevServerHelper::class.java.getProtectedFieldValue( devServerHelper, "mPackagerConnectionLock" ) if (!packagerConnectionLock) { devServerHelper.closePackagerConnection() return@launch } } catch (_: NoSuchFieldException) { // mPackagerConnectionLock was removed from the React Native in v0.63.4 val packagerClient: JSPackagerClient? = DevServerHelper::class.java.getProtectedFieldValue( devServerHelper, "mPackagerClient" ) if (packagerClient != null) { devServerHelper.closePackagerConnection() return@launch } } delay(50) } } catch (e: Exception) { Log.w("DevLauncher", "Couldn't close the packager connection: ${e.message}", e) } } } private fun tryToStopShakeDetector(currentDevSupportManager: DevSupportManager) { try { val shakeDetector: ShakeDetector = DevSupportManagerBase::class.java.getProtectedFieldValue( currentDevSupportManager, "mShakeDetector" ) shakeDetector.stop() } catch (e: Exception) { Log.w("DevLauncher", "Couldn't stop shake detector.", e) } } }