1 package expo.modules.devlauncher.react
2 
3 import android.util.Log
4 import com.facebook.react.ReactInstanceManager
5 import com.facebook.react.common.ShakeDetector
6 import com.facebook.react.devsupport.DevServerHelper
7 import com.facebook.react.devsupport.DevSupportManagerBase
8 import com.facebook.react.devsupport.DisabledDevSupportManager
9 import com.facebook.react.devsupport.interfaces.DevSupportManager
10 import com.facebook.react.packagerconnection.JSPackagerClient
11 import expo.modules.devlauncher.helpers.getProtectedFieldValue
12 import expo.modules.devlauncher.helpers.setProtectedDeclaredField
13 import expo.modules.devlauncher.koin.DevLauncherKoinComponent
14 import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
15 import expo.modules.devlauncher.rncompatibility.DevLauncherDevSupportManager
16 import kotlinx.coroutines.delay
17 import kotlinx.coroutines.launch
18 import org.koin.core.component.inject
19 
20 class DevLauncherDevSupportManagerSwapper : DevLauncherKoinComponent {
21   private val controller: DevLauncherControllerInterface by inject()
22 
swapDevSupportManagerImplnull23   fun swapDevSupportManagerImpl(
24     reactInstanceManager: ReactInstanceManager
25   ) {
26     val currentDevSupportManager = reactInstanceManager.devSupportManager
27     if (currentDevSupportManager is DevLauncherDevSupportManager) {
28       // DevSupportManager was swapped by the DevLauncherReactNativeHostHandler
29       return
30     }
31 
32     if (currentDevSupportManager is DisabledDevSupportManager) {
33       Log.i("DevLauncher", "DevSupportManager is disabled. So we don't want to override it.")
34       return
35     }
36     try {
37       val devManagerClass = DevSupportManagerBase::class.java
38       val newDevSupportManager = DevLauncherDevSupportManager(
39         applicationContext = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mApplicationContext"),
40         reactInstanceManagerHelper = devManagerClass.getProtectedFieldValue(currentDevSupportManager, DevLauncherDevSupportManager.getDevHelperInternalFieldName()),
41         packagerPathForJSBundleName = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mJSAppBundleName"),
42         enableOnCreate = true,
43         redBoxHandler = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mRedBoxHandler"),
44         devBundleDownloadListener = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mBundleDownloadListener"),
45         minNumShakes = 1,
46         customPackagerCommandHandlers = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "mCustomPackagerCommandHandlers")
47       )
48 
49       ReactInstanceManager::class.java.setProtectedDeclaredField(reactInstanceManager, "mDevSupportManager", newDevSupportManager)
50 
51       /**
52        * We need to invalidate the old packager connection.
53        * However, this connection is established in the background
54        * and we don't know when it will be available (see [DevServerHelper.openPackagerConnection]).
55        * So we just wait for connection and then we kill it.
56        */
57       controller.coroutineScope.launch {
58         try {
59           while (true) {
60             // Invalidate shake detector - not doing that leads to memory leaks
61             tryToStopShakeDetector(currentDevSupportManager)
62 
63             val devServerHelper: DevServerHelper = devManagerClass.getProtectedFieldValue(
64               currentDevSupportManager,
65               "mDevServerHelper"
66             )
67 
68             try {
69               val packagerConnectionLock: Boolean = DevServerHelper::class.java.getProtectedFieldValue(
70                 devServerHelper,
71                 "mPackagerConnectionLock"
72               )
73 
74               if (!packagerConnectionLock) {
75                 devServerHelper.closePackagerConnection()
76                 return@launch
77               }
78             } catch (_: NoSuchFieldException) {
79               // mPackagerConnectionLock was removed from the React Native in v0.63.4
80               val packagerClient: JSPackagerClient? = DevServerHelper::class.java.getProtectedFieldValue(
81                 devServerHelper,
82                 "mPackagerClient"
83               )
84 
85               if (packagerClient != null) {
86                 devServerHelper.closePackagerConnection()
87                 return@launch
88               }
89             }
90 
91             delay(50)
92           }
93         } catch (e: Exception) {
94           Log.w("DevLauncher", "Couldn't close the packager connection: ${e.message}", e)
95         }
96       }
97     } catch (e: Exception) {
98       Log.i("DevLauncher", "Couldn't inject `DevLauncherDevSupportManager`.", e)
99     }
100   }
101 
tryToStopShakeDetectornull102   private fun tryToStopShakeDetector(currentDevSupportManager: DevSupportManager) {
103     try {
104       val shakeDetector: ShakeDetector =
105         DevSupportManagerBase::class.java.getProtectedFieldValue(
106           currentDevSupportManager,
107           "mShakeDetector",
108         )
109       shakeDetector.stop()
110     } catch (e: Exception) {
111       Log.w("DevLauncher", "Couldn't stop shake detector.", e)
112     }
113   }
114 }
115