1 package expo.modules.devlauncher.helpers
2 
3 import android.content.Context
4 import android.net.Uri
5 import android.util.Log
6 import com.facebook.react.ReactNativeHost
7 import com.facebook.react.ReactPackage
8 import com.facebook.react.bridge.JSBundleLoader
9 import com.facebook.react.devsupport.DevLauncherInternalSettings
10 import expo.interfaces.devmenu.annotations.ContainsDevMenuExtension
11 import expo.modules.devlauncher.react.DevLauncherDevSupportManagerSwapper
12 import expo.modules.devlauncher.rncompatibility.DevLauncherDevSupportManager
13 import okhttp3.HttpUrl
14 
injectReactInterceptornull15 fun injectReactInterceptor(
16   context: Context,
17   reactNativeHost: ReactNativeHost,
18   url: Uri
19 ): Boolean {
20   val port = if (url.port != -1) url.port else HttpUrl.defaultPort(url.scheme)
21   val debugServerHost = url.host + ":" + port
22   // We need to remove "/" which is added to begin of the path by the Uri
23   // and the bundle type
24   val appBundleName = if (url.path.isNullOrEmpty()) {
25     "index"
26   } else {
27     url.path
28       ?.substring(1)
29       ?.replace(".bundle", "")
30       ?: "index"
31   }
32 
33   injectDevSupportManager(reactNativeHost)
34 
35   val result = injectDebugServerHost(
36     context,
37     reactNativeHost,
38     debugServerHost,
39     appBundleName
40   )
41   (reactNativeHost.reactInstanceManager.devSupportManager as? DevLauncherDevSupportManager)?.startInspectorWhenDevLauncherReady()
42 
43   return result
44 }
45 
injectDevSupportManagernull46 fun injectDevSupportManager(
47   reactNativeHost: ReactNativeHost
48 ) {
49   DevLauncherDevSupportManagerSwapper()
50     .swapDevSupportManagerImpl(reactNativeHost.reactInstanceManager)
51 }
52 
injectDebugServerHostnull53 fun injectDebugServerHost(
54   context: Context,
55   reactNativeHost: ReactNativeHost,
56   debugServerHost: String,
57   appBundleName: String
58 ): Boolean {
59   return try {
60     val instanceManager = reactNativeHost.reactInstanceManager
61     val settings = DevLauncherInternalSettings(context, debugServerHost)
62     val devSupportManager = instanceManager.devSupportManager
63     val devSupportManagerBaseClass: Class<*>? = devSupportManager.javaClass.superclass
64     devSupportManagerBaseClass!!.setProtectedDeclaredField(
65       devSupportManager,
66       "mJSAppBundleName",
67       appBundleName
68     )
69     val mDevSettingsField = devSupportManagerBaseClass.getDeclaredField("mDevSettings")
70     mDevSettingsField.isAccessible = true
71     mDevSettingsField[devSupportManager] = settings
72     val mDevServerHelperField = devSupportManagerBaseClass.getDeclaredField("mDevServerHelper")
73     mDevServerHelperField.isAccessible = true
74     val devServerHelper = mDevServerHelperField[devSupportManager]
75     val mSettingsField = devServerHelper.javaClass.getDeclaredField("mSettings")
76     mSettingsField.isAccessible = true
77     mSettingsField[devServerHelper] = settings
78     // set useDeveloperSupport to true in case it was previously set to false from loading a published app
79     val mUseDeveloperSupportField = instanceManager.javaClass.getDeclaredField("mUseDeveloperSupport")
80     mUseDeveloperSupportField.isAccessible = true
81     mUseDeveloperSupportField[instanceManager] = true
82     true
83   } catch (e: Exception) {
84     Log.e("DevLauncher", "Unable to inject debug server host settings.", e)
85     false
86   }
87 }
88 
injectLocalBundleLoadernull89 fun injectLocalBundleLoader(
90   reactNativeHost: ReactNativeHost,
91   bundlePath: String
92 ): Boolean {
93   return try {
94     val instanceManager = reactNativeHost.reactInstanceManager
95     val instanceManagerClass = instanceManager.javaClass
96 
97     val jsBundleLoader = JSBundleLoader.createFileLoader(bundlePath)
98     val mBundleLoaderField = instanceManagerClass.getDeclaredField("mBundleLoader")
99     mBundleLoaderField.isAccessible = true
100     mBundleLoaderField[instanceManager] = jsBundleLoader
101 
102     val mUseDeveloperSupportField = instanceManagerClass.getDeclaredField("mUseDeveloperSupport")
103     mUseDeveloperSupportField.isAccessible = true
104     mUseDeveloperSupportField[instanceManager] = false
105     true
106   } catch (e: Exception) {
107     Log.e("DevLauncher", "Unable to load local bundle file", e)
108     false
109   }
110 }
111 
findDevMenuPackagenull112 fun findDevMenuPackage(): ReactPackage? {
113   return try {
114     val clazz = Class.forName("expo.modules.devmenu.DevMenuPackage")
115     clazz.newInstance() as? ReactPackage
116   } catch (e: Exception) {
117     null
118   }
119 }
120 
findPackagesWithDevMenuExtensionnull121 fun findPackagesWithDevMenuExtension(reactNativeHost: ReactNativeHost): List<ReactPackage> {
122   return try {
123     val clazz = Class.forName("com.facebook.react.PackageList")
124     val ctor = clazz.getConstructor(ReactNativeHost::class.java)
125     val packageList = ctor.newInstance(reactNativeHost)
126 
127     val getPackagesMethod = packageList.javaClass.getDeclaredMethod("getPackages")
128     val packages = getPackagesMethod.invoke(packageList) as List<*>
129     return packages
130       .filterIsInstance<ReactPackage>()
131       .filter {
132         it.javaClass.isAnnotationPresent(ContainsDevMenuExtension::class.java)
133       }
134   } catch (e: Exception) {
135     Log.e("DevLauncher", "Unable find packages with dev menu extension.`.", e)
136     emptyList()
137   }
138 }
139