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