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