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