-
Notifications
You must be signed in to change notification settings - Fork 4.7k
/
DevLauncherReactUtils.kt
276 lines (250 loc) · 9.9 KB
/
DevLauncherReactUtils.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package expo.modules.devlauncher.helpers
import android.app.Application
import android.content.Context
import android.net.Uri
import android.util.Log
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
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.DevLauncherInternalSettings
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.interfaces.devmenu.ReactHostWrapper
import expo.interfaces.devmenu.annotations.ContainsDevMenuExtension
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
import expo.modules.devlauncher.react.DevLauncherDevSupportManagerSwapper
import expo.modules.devlauncher.rncompatibility.DevLauncherBridgeDevSupportManager
import expo.modules.devlauncher.rncompatibility.DevLauncherBridgelessDevSupportManager
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: ReactHostWrapper,
url: Uri
): Boolean {
val (debugServerHost, appBundleName) = parseUrl(url)
injectDevSupportManager(reactHost)
val result = injectDebugServerHost(
context,
reactHost,
debugServerHost,
appBundleName
)
if (reactHost.isBridgelessMode) {
(reactHost.devSupportManager as? DevLauncherBridgelessDevSupportManager)?.startInspectorWhenDevLauncherReady()
} else {
(reactHost.devSupportManager as? DevLauncherBridgeDevSupportManager)?.startInspectorWhenDevLauncherReady()
}
return result
}
private fun injectDevSupportManager(reactHost: ReactHostWrapper) {
DevLauncherDevSupportManagerSwapper().swapDevSupportManagerImpl(reactHost)
}
fun injectDebugServerHost(
context: Context,
reactHost: ReactHostWrapper,
debugServerHost: String,
appBundleName: String
): Boolean {
if (reactHost.isBridgelessMode) {
return injectDebugServerHost(context, reactHost.reactHost, debugServerHost, appBundleName)
} else {
return injectDebugServerHost(context, reactHost.reactNativeHost, debugServerHost, appBundleName)
}
}
fun injectDebugServerHost(
context: Context,
reactNativeHost: ReactNativeHost,
debugServerHost: String,
appBundleName: String
): Boolean {
return try {
val instanceManager = reactNativeHost.reactInstanceManager
val devSupportManager = instanceManager.devSupportManager
injectDebugServerHost(context, devSupportManager, debugServerHost, appBundleName)
// set useDeveloperSupport to true in case it was previously set to false from loading a published app
val mUseDeveloperSupportField = instanceManager.javaClass.getDeclaredField("mUseDeveloperSupport")
mUseDeveloperSupportField.isAccessible = true
mUseDeveloperSupportField[instanceManager] = true
true
} catch (e: Exception) {
Log.e("DevLauncher", "Unable to inject debug server host settings.", e)
false
}
}
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 = DevLauncherInternalSettings(context, debugServerHost)
val devSupportManagerBaseClass: Class<*> = DevSupportManagerBase::class.java
devSupportManagerBaseClass.setProtectedDeclaredField(
devSupportManager,
"mJSAppBundleName",
appBundleName
)
val mDevSettingsField = devSupportManagerBaseClass.getDeclaredField("mDevSettings")
mDevSettingsField.isAccessible = true
mDevSettingsField[devSupportManager] = settings
val mDevServerHelperField = devSupportManagerBaseClass.getDeclaredField("mDevServerHelper")
mDevServerHelperField.isAccessible = true
val devServerHelper = mDevServerHelperField[devSupportManager]
check(devServerHelper is DevLauncherDevServerHelper)
val mSettingsField = DevServerHelper::class.java.getDeclaredField("mSettings")
mSettingsField.isAccessible = true
mSettingsField[devServerHelper] = settings
val packagerConnectionSettingsField = DevServerHelper::class.java.getDeclaredField("mPackagerConnectionSettings")
packagerConnectionSettingsField.isAccessible = true
packagerConnectionSettingsField[devServerHelper] = settings.public_getPackagerConnectionSettings()
}
fun injectLocalBundleLoader(
reactHost: ReactHostWrapper,
bundlePath: String
): Boolean {
return if (reactHost.isBridgelessMode) {
injectLocalBundleLoader(reactHost.reactHost, bundlePath)
} else {
injectLocalBundleLoader(reactHost.reactNativeHost, bundlePath)
}
}
private fun injectLocalBundleLoader(
reactNativeHost: ReactNativeHost,
bundlePath: String
): Boolean {
return try {
val instanceManager = reactNativeHost.reactInstanceManager
val instanceManagerClass = instanceManager.javaClass
val jsBundleLoader = JSBundleLoader.createFileLoader(bundlePath)
val mBundleLoaderField = instanceManagerClass.getDeclaredField("mBundleLoader")
mBundleLoaderField.isAccessible = true
mBundleLoaderField[instanceManager] = jsBundleLoader
val mUseDeveloperSupportField = instanceManagerClass.getDeclaredField("mUseDeveloperSupport")
mUseDeveloperSupportField.isAccessible = true
mUseDeveloperSupportField[instanceManager] = false
true
} catch (e: Exception) {
Log.e("DevLauncher", "Unable to load local bundle file", e)
false
}
}
@OptIn(UnstableReactNativeAPI::class)
private fun injectLocalBundleLoader(
reactHost: ReactHost,
bundlePath: String
): 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("mAllowPackagerServerAccess")
mAllowPackagerServerAccessField.isAccessible = true
mAllowPackagerServerAccessField[reactHost] = false
val newJsBundleLoader = JSBundleLoader.createFileLoader(bundlePath)
// [1] Replace the ReactHostDelegate.jsBundlerLoader with our new loader
val mReactHostDelegateField = reactHostClass.getDeclaredField("mReactHostDelegate")
mReactHostDelegateField.isAccessible = true
val reactHostDelegate = mReactHostDelegateField[reactHost] as ReactHostDelegate
if (reactHostDelegate.javaClass.canonicalName == EXPO_REACT_HOST_DELEGATE_CLASS) {
reactHostDelegate.javaClass.setPrivateDeclaredFieldValue(
"_jsBundleLoader",
reactHostDelegate,
newJsBundleLoader
)
} else if (reactHostDelegate is DefaultReactHostDelegate) {
DefaultReactHostDelegate::class.java.setPrivateDeclaredFieldValue(
"jsBundleLoader",
reactHostDelegate,
newJsBundleLoader
)
} else {
throw IllegalStateException("[injectLocalBundleLoader] Unsupported reactHostDelegate: ${reactHostDelegate.javaClass}")
}
true
} catch (e: Exception) {
Log.e("DevLauncher", "Unable to load local bundle file", e)
false
}
}
fun injectDevServerHelper(context: Context, devSupportManager: DevSupportManager, controller: DevLauncherControllerInterface?) {
val defaultServerHost = AndroidInfoHelpers.getServerHost(context)
val devSettings = DevLauncherInternalSettings(context, defaultServerHost)
val devLauncherDevServerHelper = DevLauncherDevServerHelper(
context = context,
controller = controller,
devSettings = devSettings,
packagerConnection = devSettings.public_getPackagerConnectionSettings()
)
DevSupportManagerBase::class.java.setProtectedDeclaredField(
devSupportManager,
"mDevServerHelper",
devLauncherDevServerHelper
)
}
fun findDevMenuPackage(): ReactPackage? {
return try {
val clazz = Class.forName("expo.modules.devmenu.DevMenuPackage")
clazz.newInstance() as? ReactPackage
} catch (e: Exception) {
null
}
}
fun findPackagesWithDevMenuExtension(application: Application): List<ReactPackage> {
return try {
val clazz = Class.forName("com.facebook.react.PackageList")
val ctor = clazz.getConstructor(Application::class.java)
val packageList = ctor.newInstance(application)
val getPackagesMethod = packageList.javaClass.getDeclaredMethod("getPackages")
val packages = getPackagesMethod.invoke(packageList) as List<*>
return packages
.filterIsInstance<ReactPackage>()
.filter {
it.javaClass.isAnnotationPresent(ContainsDevMenuExtension::class.java)
}
} catch (e: Exception) {
Log.e("DevLauncher", "Unable find packages with dev menu extension.`.", e)
emptyList()
}
}
private fun parseUrl(url: Uri): Pair<String, String> {
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)
}