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