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.provider.Settings 8 import com.facebook.common.logging.FLog 9 import com.facebook.hermes.reactexecutor.HermesExecutorFactory 10 import com.facebook.react.ReactInstanceManager 11 import com.facebook.react.ReactInstanceManagerBuilder 12 import com.facebook.react.bridge.JavaScriptContextHolder 13 import com.facebook.react.bridge.JavaScriptExecutorFactory 14 import com.facebook.react.bridge.ReactApplicationContext 15 import com.facebook.react.common.LifecycleState 16 import com.facebook.react.common.ReactConstants 17 import com.facebook.react.jscexecutor.JSCExecutorFactory 18 import com.facebook.react.modules.systeminfo.AndroidInfoHelpers 19 import com.facebook.react.packagerconnection.NotificationOnlyHandler 20 import com.facebook.react.packagerconnection.RequestHandler 21 import com.facebook.react.shell.MainReactPackage 22 import expo.modules.jsonutils.getNullable 23 import host.exp.exponent.RNObject 24 import host.exp.exponent.experience.ExperienceActivity 25 import host.exp.exponent.experience.ReactNativeActivity 26 import host.exp.expoview.Exponent 27 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties 28 import org.json.JSONObject 29 import java.util.* 30 31 object VersionedUtils { 32 private fun toggleExpoDevMenu() { 33 val currentActivity = Exponent.instance.currentActivity 34 if (currentActivity is ExperienceActivity) { 35 currentActivity.toggleDevMenu() 36 } else { 37 FLog.e( 38 ReactConstants.TAG, 39 "Unable to toggle the Expo dev menu because the current activity could not be found." 40 ) 41 } 42 } 43 44 private fun reloadExpoApp() { 45 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 46 FLog.e( 47 ReactConstants.TAG, 48 "Unable to reload the app because the current activity could not be found." 49 ) 50 } 51 val devSupportManager = currentActivity.devSupportManager ?: return run { 52 FLog.e( 53 ReactConstants.TAG, 54 "Unable to get the DevSupportManager from current activity." 55 ) 56 } 57 58 devSupportManager.callRecursive("reloadExpoApp") 59 } 60 61 private fun toggleElementInspector() { 62 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 63 FLog.e( 64 ReactConstants.TAG, 65 "Unable to toggle the element inspector because the current activity could not be found." 66 ) 67 } 68 val devSupportManager = currentActivity.devSupportManager ?: return run { 69 FLog.e( 70 ReactConstants.TAG, 71 "Unable to get the DevSupportManager from current activity." 72 ) 73 } 74 75 devSupportManager.callRecursive("toggleElementInspector") 76 } 77 78 private fun requestOverlayPermission(context: Context) { 79 // From the unexposed DebugOverlayController static helper 80 // Get permission to show debug overlay in dev builds. 81 if (!Settings.canDrawOverlays(context)) { 82 val intent = Intent( 83 Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 84 Uri.parse("package:" + context.packageName) 85 ).apply { 86 flags = Intent.FLAG_ACTIVITY_NEW_TASK 87 } 88 FLog.w( 89 ReactConstants.TAG, 90 "Overlay permissions needs to be granted in order for React Native apps to run in development mode" 91 ) 92 if (intent.resolveActivity(context.packageManager) != null) { 93 context.startActivity(intent) 94 } 95 } 96 } 97 98 private fun togglePerformanceMonitor() { 99 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 100 FLog.e( 101 ReactConstants.TAG, 102 "Unable to toggle the performance monitor because the current activity could not be found." 103 ) 104 } 105 val devSupportManager = currentActivity.devSupportManager ?: return run { 106 FLog.e( 107 ReactConstants.TAG, 108 "Unable to get the DevSupportManager from current activity." 109 ) 110 } 111 112 val devSettings = devSupportManager.callRecursive("getDevSettings") 113 if (devSettings != null) { 114 val isFpsDebugEnabled = devSettings.call("isFpsDebugEnabled") as Boolean 115 if (!isFpsDebugEnabled) { 116 // Request overlay permission if needed when "Show Perf Monitor" option is selected 117 requestOverlayPermission(currentActivity) 118 } 119 devSettings.call("setFpsDebugEnabled", !isFpsDebugEnabled) 120 } 121 } 122 123 private fun toggleRemoteJSDebugging() { 124 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 125 FLog.e( 126 ReactConstants.TAG, 127 "Unable to toggle remote JS debugging because the current activity could not be found." 128 ) 129 } 130 val devSupportManager = currentActivity.devSupportManager ?: return run { 131 FLog.e( 132 ReactConstants.TAG, 133 "Unable to get the DevSupportManager from current activity." 134 ) 135 } 136 137 val devSettings = devSupportManager.callRecursive("getDevSettings") 138 if (devSettings != null) { 139 val isRemoteJSDebugEnabled = devSettings.call("isRemoteJSDebugEnabled") as Boolean 140 devSettings.call("setRemoteJSDebugEnabled", !isRemoteJSDebugEnabled) 141 } 142 } 143 144 private fun reconnectReactDevTools() { 145 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 146 FLog.e( 147 ReactConstants.TAG, 148 "Unable to get the current activity." 149 ) 150 } 151 // Emit the `RCTDevMenuShown` for the app to reconnect react-devtools 152 // https://github.com/facebook/react-native/blob/22ba1e45c52edcc345552339c238c1f5ef6dfc65/Libraries/Core/setUpReactDevTools.js#L80 153 currentActivity.emitRCTNativeAppEvent("RCTDevMenuShown", null) 154 } 155 156 private fun createPackagerCommandHelpers(): Map<String, RequestHandler> { 157 // Attach listeners to the bundler's dev server web socket connection. 158 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 159 val packagerCommandHandlers = mutableMapOf<String, RequestHandler>() 160 161 // Enable a lot of tools under the same command namespace 162 packagerCommandHandlers["sendDevCommand"] = object : NotificationOnlyHandler() { 163 override fun onNotification(params: Any?) { 164 if (params != null && params is JSONObject) { 165 when (params.getNullable<String>("name")) { 166 "reload" -> reloadExpoApp() 167 "toggleDevMenu" -> toggleExpoDevMenu() 168 "toggleRemoteDebugging" -> { 169 toggleRemoteJSDebugging() 170 // Reload the app after toggling debugging, this is based on what we do in DevSupportManagerBase. 171 reloadExpoApp() 172 } 173 "toggleElementInspector" -> toggleElementInspector() 174 "togglePerformanceMonitor" -> togglePerformanceMonitor() 175 "reconnectReactDevTools" -> reconnectReactDevTools() 176 } 177 } 178 } 179 } 180 181 // These commands (reload and devMenu) are here to match RN dev tooling. 182 183 // Reload the app on "reload" 184 packagerCommandHandlers["reload"] = object : NotificationOnlyHandler() { 185 override fun onNotification(params: Any?) { 186 reloadExpoApp() 187 } 188 } 189 190 // Open the dev menu on "devMenu" 191 packagerCommandHandlers["devMenu"] = object : NotificationOnlyHandler() { 192 override fun onNotification(params: Any?) { 193 toggleExpoDevMenu() 194 } 195 } 196 197 return packagerCommandHandlers 198 } 199 200 @JvmStatic fun getReactInstanceManagerBuilder(instanceManagerBuilderProperties: InstanceManagerBuilderProperties): ReactInstanceManagerBuilder { 201 // Build the instance manager 202 var builder = ReactInstanceManager.builder() 203 .setApplication(instanceManagerBuilderProperties.application) 204 .setJSIModulesPackage { reactApplicationContext: ReactApplicationContext, jsContext: JavaScriptContextHolder? -> 205 emptyList() 206 } 207 .addPackage(MainReactPackage()) 208 .addPackage( 209 ExponentPackage( 210 instanceManagerBuilderProperties.experienceProperties, 211 instanceManagerBuilderProperties.manifest, 212 // DO NOT EDIT THIS COMMENT - used by versioning scripts 213 // When distributing change the following two arguments to nulls 214 instanceManagerBuilderProperties.expoPackages, 215 instanceManagerBuilderProperties.exponentPackageDelegate, 216 instanceManagerBuilderProperties.singletonModules 217 ) 218 ) 219 .addPackage( 220 ExpoTurboPackage( 221 instanceManagerBuilderProperties.experienceProperties, 222 instanceManagerBuilderProperties.manifest 223 ) 224 ) 225 .setMinNumShakes(100) // disable the RN dev menu 226 .setInitialLifecycleState(LifecycleState.BEFORE_CREATE) 227 .setCustomPackagerCommandHandlers(createPackagerCommandHelpers()) 228 .setJavaScriptExecutorFactory(createJSExecutorFactory(instanceManagerBuilderProperties)) 229 if (instanceManagerBuilderProperties.jsBundlePath != null && instanceManagerBuilderProperties.jsBundlePath!!.isNotEmpty()) { 230 builder = builder.setJSBundleFile(instanceManagerBuilderProperties.jsBundlePath) 231 } 232 return builder 233 } 234 235 private fun getDevSupportManager(reactApplicationContext: ReactApplicationContext): RNObject? { 236 val currentActivity = Exponent.instance.currentActivity 237 return if (currentActivity != null) { 238 if (currentActivity is ReactNativeActivity) { 239 currentActivity.devSupportManager 240 } else { 241 null 242 } 243 } else try { 244 val devSettingsModule = reactApplicationContext.catalystInstance.getNativeModule("DevSettings") 245 val devSupportManagerField = devSettingsModule!!.javaClass.getDeclaredField("mDevSupportManager") 246 devSupportManagerField.isAccessible = true 247 RNObject.wrap(devSupportManagerField[devSettingsModule]!!) 248 } catch (e: Throwable) { 249 e.printStackTrace() 250 null 251 } 252 } 253 254 private fun createJSExecutorFactory( 255 instanceManagerBuilderProperties: InstanceManagerBuilderProperties 256 ): JavaScriptExecutorFactory? { 257 val appName = instanceManagerBuilderProperties.manifest.getName() ?: "" 258 val deviceName = AndroidInfoHelpers.getFriendlyDeviceName() 259 260 val jsEngineFromManifest = instanceManagerBuilderProperties.manifest.jsEngine 261 return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory( 262 appName, 263 deviceName 264 ) 265 } 266 } 267