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