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