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 = 76
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       .setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
245       .setCustomPackagerCommandHandlers(createPackagerCommandHelpers())
246       .setJavaScriptExecutorFactory(createJSExecutorFactory(instanceManagerBuilderProperties))
247     if (instanceManagerBuilderProperties.jsBundlePath != null && instanceManagerBuilderProperties.jsBundlePath!!.isNotEmpty()) {
248       builder = builder.setJSBundleFile(instanceManagerBuilderProperties.jsBundlePath)
249     }
250     return builder
251   }
252 
253   private fun getDevSupportManager(reactApplicationContext: ReactApplicationContext): RNObject? {
254     val currentActivity = Exponent.instance.currentActivity
255     return if (currentActivity != null) {
256       if (currentActivity is ReactNativeActivity) {
257         currentActivity.devSupportManager
258       } else {
259         null
260       }
261     } else try {
262       val devSettingsModule = reactApplicationContext.catalystInstance.getNativeModule("DevSettings")
263       val devSupportManagerField = devSettingsModule!!.javaClass.getDeclaredField("mDevSupportManager")
264       devSupportManagerField.isAccessible = true
265       RNObject.wrap(devSupportManagerField[devSettingsModule]!!)
266     } catch (e: Throwable) {
267       e.printStackTrace()
268       null
269     }
270   }
271 
272   private fun createJSExecutorFactory(
273     instanceManagerBuilderProperties: InstanceManagerBuilderProperties
274   ): JavaScriptExecutorFactory? {
275     val appName = instanceManagerBuilderProperties.manifest.getName() ?: ""
276     val deviceName = AndroidInfoHelpers.getFriendlyDeviceName()
277 
278     if (Constants.isStandaloneApp()) {
279       return JSCExecutorFactory(appName, deviceName)
280     }
281 
282     val hermesBundlePair = parseHermesBundleHeader(instanceManagerBuilderProperties.jsBundlePath)
283     if (hermesBundlePair.first && hermesBundlePair.second != HERMES_BYTECODE_VERSION) {
284       val message = String.format(
285         Locale.US,
286         "Unable to load unsupported Hermes bundle.\n\tsupportedBytecodeVersion: %d\n\ttargetBytecodeVersion: %d",
287         HERMES_BYTECODE_VERSION, hermesBundlePair.second
288       )
289       KernelProvider.instance.handleError(RuntimeException(message))
290       return null
291     }
292     val jsEngineFromManifest = instanceManagerBuilderProperties.manifest.getAndroidJsEngine()
293     return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory(
294       appName,
295       deviceName
296     )
297   }
298 
299   private fun parseHermesBundleHeader(jsBundlePath: String?): Pair<Boolean, Int> {
300     if (jsBundlePath == null || jsBundlePath.isEmpty()) {
301       return Pair(false, 0)
302     }
303 
304     // https://github.com/facebook/hermes/blob/release-v0.5/include/hermes/BCGen/HBC/BytecodeFileFormat.h#L24-L25
305     val HERMES_MAGIC_HEADER = byteArrayOf(
306       0xc6.toByte(), 0x1f.toByte(), 0xbc.toByte(), 0x03.toByte(),
307       0xc1.toByte(), 0x03.toByte(), 0x19.toByte(), 0x1f.toByte()
308     )
309     val file = File(jsBundlePath)
310     try {
311       FileInputStream(file).use { inputStream ->
312         val bytes = ByteArray(12)
313         inputStream.read(bytes, 0, bytes.size)
314 
315         // Magic header
316         for (i in HERMES_MAGIC_HEADER.indices) {
317           if (bytes[i] != HERMES_MAGIC_HEADER[i]) {
318             return Pair(false, 0)
319           }
320         }
321 
322         // Bytecode version
323         val bundleBytecodeVersion: Int =
324           (bytes[11].toInt() shl 24) or (bytes[10].toInt() shl 16) or (bytes[9].toInt() shl 8) or bytes[8].toInt()
325         return Pair(true, bundleBytecodeVersion)
326       }
327     } catch (e: FileNotFoundException) {
328     } catch (e: IOException) {
329     }
330 
331     return Pair(false, 0)
332   }
333 
334   internal fun isHermesBundle(jsBundlePath: String?): Boolean {
335     return parseHermesBundleHeader(jsBundlePath).first
336   }
337 
338   internal fun getHermesBundleBytecodeVersion(jsBundlePath: String?): Int {
339     return parseHermesBundleHeader(jsBundlePath).second
340   }
341 }
342