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