1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package versioned.host.exp.exponent
3 
4 import android.content.Context
5 import android.content.Intent
6 import android.net.Uri
7 import android.os.Build
8 import android.provider.Settings
9 import android.util.Log
10 import com.facebook.common.logging.FLog
11 import com.facebook.hermes.reactexecutor.HermesExecutorFactory
12 import com.facebook.react.ReactInstanceManager
13 import com.facebook.react.ReactInstanceManagerBuilder
14 import com.facebook.react.bridge.JavaScriptContextHolder
15 import com.facebook.react.bridge.JavaScriptExecutorFactory
16 import com.facebook.react.bridge.ReactApplicationContext
17 import com.facebook.react.common.LifecycleState
18 import com.facebook.react.common.ReactConstants
19 import com.facebook.react.jscexecutor.JSCExecutorFactory
20 import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
21 import com.facebook.react.packagerconnection.NotificationOnlyHandler
22 import com.facebook.react.packagerconnection.RequestHandler
23 import com.facebook.react.shell.MainReactPackage
24 import host.exp.exponent.Constants
25 import host.exp.exponent.RNObject
26 import host.exp.exponent.experience.ExperienceActivity
27 import host.exp.exponent.experience.ReactNativeActivity
28 import host.exp.exponent.kernel.KernelProvider
29 import host.exp.expoview.Exponent
30 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties
31 import org.json.JSONObject
32 import versioned.host.exp.exponent.modules.api.reanimated.ReanimatedJSIModulePackage
33 import java.io.File
34 import java.io.FileInputStream
35 import java.io.FileNotFoundException
36 import java.io.IOException
37 import java.util.*
38 
39 object VersionedUtils {
40   // Update this value when hermes-engine getting updated.
41   // Currently there is no way to retrieve Hermes bytecode version from Java,
42   // as an alternative, we maintain the version by hand.
43   private const val HERMES_BYTECODE_VERSION = 74
44 
45   private fun toggleExpoDevMenu() {
46     val currentActivity = Exponent.instance.currentActivity
47     if (currentActivity is ExperienceActivity) {
48       currentActivity.toggleDevMenu()
49     } else {
50       FLog.e(
51         ReactConstants.TAG,
52         "Unable to toggle the Expo dev menu because the current activity could not be found."
53       )
54     }
55   }
56 
57   private fun reloadExpoApp() {
58     val currentActivity = Exponent.instance.currentActivity
59     if (currentActivity is ReactNativeActivity) {
60       currentActivity.devSupportManager.callRecursive("reloadExpoApp")
61     } else {
62       FLog.e(
63         ReactConstants.TAG,
64         "Unable to reload the app because the current activity could not be found."
65       )
66     }
67   }
68 
69   private fun toggleElementInspector() {
70     val currentActivity = Exponent.instance.currentActivity
71     if (currentActivity is ReactNativeActivity) {
72       currentActivity.devSupportManager.callRecursive("toggleElementInspector")
73     } else {
74       FLog.e(
75         ReactConstants.TAG,
76         "Unable to toggle the element inspector because the current activity could not be found."
77       )
78     }
79   }
80 
81   private fun requestOverlayPermission(context: Context) {
82     // From the unexposed DebugOverlayController static helper
83     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
84       // Get permission to show debug overlay in dev builds.
85       if (!Settings.canDrawOverlays(context)) {
86         val intent = Intent(
87           Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
88           Uri.parse("package:" + context.packageName)
89         ).apply {
90           flags = Intent.FLAG_ACTIVITY_NEW_TASK
91         }
92         FLog.w(
93           ReactConstants.TAG,
94           "Overlay permissions needs to be granted in order for React Native apps to run in development mode"
95         )
96         if (intent.resolveActivity(context.packageManager) != null) {
97           context.startActivity(intent)
98         }
99       }
100     }
101   }
102 
103   private fun togglePerformanceMonitor() {
104     val currentActivity = Exponent.instance.currentActivity
105     if (currentActivity is ReactNativeActivity) {
106       val devSettings = currentActivity.devSupportManager.callRecursive("getDevSettings")
107       if (devSettings != null) {
108         val isFpsDebugEnabled = devSettings.call("isFpsDebugEnabled") as Boolean
109         if (!isFpsDebugEnabled) {
110           // Request overlay permission if needed when "Show Perf Monitor" option is selected
111           requestOverlayPermission(currentActivity)
112         }
113         devSettings.call("setFpsDebugEnabled", !isFpsDebugEnabled)
114       }
115     } else {
116       FLog.e(
117         ReactConstants.TAG,
118         "Unable to toggle the performance monitor because the current activity could not be found."
119       )
120     }
121   }
122 
123   private fun toggleRemoteJSDebugging() {
124     val currentActivity = Exponent.instance.currentActivity
125     if (currentActivity is ReactNativeActivity) {
126       val devSettings = currentActivity.devSupportManager.callRecursive("getDevSettings")
127       if (devSettings != null) {
128         val isRemoteJSDebugEnabled = devSettings.call("isRemoteJSDebugEnabled") as Boolean
129         devSettings.call("setRemoteJSDebugEnabled", !isRemoteJSDebugEnabled)
130       }
131     } else {
132       FLog.e(
133         ReactConstants.TAG,
134         "Unable to toggle remote JS debugging because the current activity could not be found."
135       )
136     }
137   }
138 
139   private fun createPackagerCommandHelpers(): Map<String, RequestHandler> {
140     // Attach listeners to the bundler's dev server web socket connection.
141     // This enables tools to automatically reload the client remotely (i.e. in expo-cli).
142     val packagerCommandHandlers = mutableMapOf<String, RequestHandler>()
143 
144     // Enable a lot of tools under the same command namespace
145     packagerCommandHandlers["sendDevCommand"] = object : NotificationOnlyHandler() {
146       override fun onNotification(params: Any?) {
147         if (params != null && params is JSONObject) {
148           val name = if (params.has("name")) {
149             params.optString("name")
150           } else null
151 
152           when (name) {
153             "reload" -> reloadExpoApp()
154             "toggleDevMenu" -> toggleExpoDevMenu()
155             "toggleRemoteDebugging" -> {
156               toggleRemoteJSDebugging()
157               // Reload the app after toggling debugging, this is based on what we do in DevSupportManagerBase.
158               reloadExpoApp()
159             }
160             "toggleElementInspector" -> toggleElementInspector()
161             "togglePerformanceMonitor" -> togglePerformanceMonitor()
162           }
163         }
164       }
165     }
166 
167     // These commands (reload and devMenu) are here to match RN dev tooling.
168 
169     // Reload the app on "reload"
170     packagerCommandHandlers["reload"] = object : NotificationOnlyHandler() {
171       override fun onNotification(params: Any?) {
172         reloadExpoApp()
173       }
174     }
175 
176     // Open the dev menu on "devMenu"
177     packagerCommandHandlers["devMenu"] = object : NotificationOnlyHandler() {
178       override fun onNotification(params: Any?) {
179         toggleExpoDevMenu()
180       }
181     }
182 
183     return packagerCommandHandlers
184   }
185 
186   @JvmStatic fun getReactInstanceManagerBuilder(instanceManagerBuilderProperties: InstanceManagerBuilderProperties): ReactInstanceManagerBuilder {
187     // Build the instance manager
188     var builder = ReactInstanceManager.builder()
189       .setApplication(instanceManagerBuilderProperties.application)
190       .setJSIModulesPackage { reactApplicationContext: ReactApplicationContext, jsContext: JavaScriptContextHolder? ->
191         val devSupportManager = getDevSupportManager(reactApplicationContext)
192         if (devSupportManager == null) {
193           Log.e(
194             "Exponent",
195             "Couldn't get the `DevSupportManager`. JSI modules won't be initialized."
196           )
197           return@setJSIModulesPackage emptyList()
198         }
199         val devSettings = devSupportManager.callRecursive("getDevSettings")
200         val isRemoteJSDebugEnabled = devSettings != null && devSettings.call("isRemoteJSDebugEnabled") as Boolean
201         if (!isRemoteJSDebugEnabled) {
202           return@setJSIModulesPackage ReanimatedJSIModulePackage().getJSIModules(
203             reactApplicationContext,
204             jsContext
205           )
206         }
207         emptyList()
208       }
209       .addPackage(MainReactPackage())
210       .addPackage(
211         ExponentPackage(
212           instanceManagerBuilderProperties.experienceProperties,
213           instanceManagerBuilderProperties.manifest,
214           // DO NOT EDIT THIS COMMENT - used by versioning scripts
215           // When distributing change the following two arguments to nulls
216           instanceManagerBuilderProperties.expoPackages,
217           instanceManagerBuilderProperties.exponentPackageDelegate,
218           instanceManagerBuilderProperties.singletonModules
219         )
220       )
221       .addPackage(
222         ExpoTurboPackage(
223           instanceManagerBuilderProperties.experienceProperties,
224           instanceManagerBuilderProperties.manifest
225         )
226       )
227       .setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
228       .setCustomPackagerCommandHandlers(createPackagerCommandHelpers())
229       .setJavaScriptExecutorFactory(createJSExecutorFactory(instanceManagerBuilderProperties))
230     if (instanceManagerBuilderProperties.jsBundlePath != null && instanceManagerBuilderProperties.jsBundlePath!!.isNotEmpty()) {
231       builder = builder.setJSBundleFile(instanceManagerBuilderProperties.jsBundlePath)
232     }
233     return builder
234   }
235 
236   private fun getDevSupportManager(reactApplicationContext: ReactApplicationContext): RNObject? {
237     val currentActivity = Exponent.instance.currentActivity
238     return if (currentActivity != null) {
239       if (currentActivity is ReactNativeActivity) {
240         currentActivity.devSupportManager
241       } else {
242         null
243       }
244     } else try {
245       val devSettingsModule = reactApplicationContext.catalystInstance.getNativeModule("DevSettings")
246       val devSupportManagerField = devSettingsModule!!.javaClass.getDeclaredField("mDevSupportManager")
247       devSupportManagerField.isAccessible = true
248       RNObject.wrap(devSupportManagerField[devSettingsModule]!!)
249     } catch (e: Throwable) {
250       e.printStackTrace()
251       null
252     }
253   }
254 
255   private fun createJSExecutorFactory(
256     instanceManagerBuilderProperties: InstanceManagerBuilderProperties
257   ): JavaScriptExecutorFactory? {
258     val appName = instanceManagerBuilderProperties.manifest.getName() ?: ""
259     val deviceName = AndroidInfoHelpers.getFriendlyDeviceName()
260 
261     if (Constants.isStandaloneApp()) {
262       return JSCExecutorFactory(appName, deviceName)
263     }
264 
265     val hermesBundlePair = parseHermesBundleHeader(instanceManagerBuilderProperties.jsBundlePath)
266     if (hermesBundlePair.first && hermesBundlePair.second != HERMES_BYTECODE_VERSION) {
267       val message = String.format(
268         Locale.US,
269         "Unable to load unsupported Hermes bundle.\n\tsupportedBytecodeVersion: %d\n\ttargetBytecodeVersion: %d",
270         HERMES_BYTECODE_VERSION, hermesBundlePair.second
271       )
272       KernelProvider.instance.handleError(RuntimeException(message))
273       return null
274     }
275     val jsEngineFromManifest = instanceManagerBuilderProperties.manifest.getAndroidJsEngine()
276     return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory(
277       appName,
278       deviceName
279     )
280   }
281 
282   private fun parseHermesBundleHeader(jsBundlePath: String?): Pair<Boolean, Int> {
283     if (jsBundlePath == null || jsBundlePath.isEmpty()) {
284       return Pair(false, 0)
285     }
286 
287     // https://github.com/facebook/hermes/blob/release-v0.5/include/hermes/BCGen/HBC/BytecodeFileFormat.h#L24-L25
288     val HERMES_MAGIC_HEADER = byteArrayOf(
289       0xc6.toByte(), 0x1f.toByte(), 0xbc.toByte(), 0x03.toByte(),
290       0xc1.toByte(), 0x03.toByte(), 0x19.toByte(), 0x1f.toByte()
291     )
292     val file = File(jsBundlePath)
293     try {
294       FileInputStream(file).use { inputStream ->
295         val bytes = ByteArray(12)
296         inputStream.read(bytes, 0, bytes.size)
297 
298         // Magic header
299         for (i in HERMES_MAGIC_HEADER.indices) {
300           if (bytes[i] != HERMES_MAGIC_HEADER[i]) {
301             return Pair(false, 0)
302           }
303         }
304 
305         // Bytecode version
306         val bundleBytecodeVersion: Int =
307           (bytes[11].toInt() shl 24) or (bytes[10].toInt() shl 16) or (bytes[9].toInt() shl 8) or bytes[8].toInt()
308         return Pair(true, bundleBytecodeVersion)
309       }
310     } catch (e: FileNotFoundException) {
311     } catch (e: IOException) {
312     }
313 
314     return Pair(false, 0)
315   }
316 
317   internal fun isHermesBundle(jsBundlePath: String?): Boolean {
318     return parseHermesBundleHeader(jsBundlePath).first
319   }
320 
321   internal fun getHermesBundleBytecodeVersion(jsBundlePath: String?): Int {
322     return parseHermesBundleHeader(jsBundlePath).second
323   }
324 }
325