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.RNObject 26 import host.exp.exponent.experience.ExperienceActivity 27 import host.exp.exponent.experience.ReactNativeActivity 28 import host.exp.expoview.Exponent 29 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties 30 import org.json.JSONObject 31 import versioned.host.exp.exponent.modules.api.reanimated.ReanimatedJSIModulePackage 32 import java.util.* 33 34 object VersionedUtils { 35 private fun toggleExpoDevMenu() { 36 val currentActivity = Exponent.instance.currentActivity 37 if (currentActivity is ExperienceActivity) { 38 currentActivity.toggleDevMenu() 39 } else { 40 FLog.e( 41 ReactConstants.TAG, 42 "Unable to toggle the Expo dev menu because the current activity could not be found." 43 ) 44 } 45 } 46 47 private fun reloadExpoApp() { 48 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 49 FLog.e( 50 ReactConstants.TAG, 51 "Unable to reload the app because the current activity could not be found." 52 ) 53 } 54 val devSupportManager = currentActivity.devSupportManager ?: return run { 55 FLog.e( 56 ReactConstants.TAG, 57 "Unable to get the DevSupportManager from current activity." 58 ) 59 } 60 61 devSupportManager.callRecursive("reloadExpoApp") 62 } 63 64 private fun toggleElementInspector() { 65 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 66 FLog.e( 67 ReactConstants.TAG, 68 "Unable to toggle the element inspector because the current activity could not be found." 69 ) 70 } 71 val devSupportManager = currentActivity.devSupportManager ?: return run { 72 FLog.e( 73 ReactConstants.TAG, 74 "Unable to get the DevSupportManager from current activity." 75 ) 76 } 77 78 devSupportManager.callRecursive("toggleElementInspector") 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 as? ReactNativeActivity ?: return run { 105 FLog.e( 106 ReactConstants.TAG, 107 "Unable to toggle the performance monitor because the current activity could not be found." 108 ) 109 } 110 val devSupportManager = currentActivity.devSupportManager ?: return run { 111 FLog.e( 112 ReactConstants.TAG, 113 "Unable to get the DevSupportManager from current activity." 114 ) 115 } 116 117 val devSettings = devSupportManager.callRecursive("getDevSettings") 118 if (devSettings != null) { 119 val isFpsDebugEnabled = devSettings.call("isFpsDebugEnabled") as Boolean 120 if (!isFpsDebugEnabled) { 121 // Request overlay permission if needed when "Show Perf Monitor" option is selected 122 requestOverlayPermission(currentActivity) 123 } 124 devSettings.call("setFpsDebugEnabled", !isFpsDebugEnabled) 125 } 126 } 127 128 private fun toggleRemoteJSDebugging() { 129 val currentActivity = Exponent.instance.currentActivity as? ReactNativeActivity ?: return run { 130 FLog.e( 131 ReactConstants.TAG, 132 "Unable to toggle remote JS debugging because the current activity could not be found." 133 ) 134 } 135 val devSupportManager = currentActivity.devSupportManager ?: return run { 136 FLog.e( 137 ReactConstants.TAG, 138 "Unable to get the DevSupportManager from current activity." 139 ) 140 } 141 142 val devSettings = devSupportManager.callRecursive("getDevSettings") 143 if (devSettings != null) { 144 val isRemoteJSDebugEnabled = devSettings.call("isRemoteJSDebugEnabled") as Boolean 145 devSettings.call("setRemoteJSDebugEnabled", !isRemoteJSDebugEnabled) 146 } 147 } 148 149 private fun createPackagerCommandHelpers(): Map<String, RequestHandler> { 150 // Attach listeners to the bundler's dev server web socket connection. 151 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 152 val packagerCommandHandlers = mutableMapOf<String, RequestHandler>() 153 154 // Enable a lot of tools under the same command namespace 155 packagerCommandHandlers["sendDevCommand"] = object : NotificationOnlyHandler() { 156 override fun onNotification(params: Any?) { 157 if (params != null && params is JSONObject) { 158 when (params.getNullable<String>("name")) { 159 "reload" -> reloadExpoApp() 160 "toggleDevMenu" -> toggleExpoDevMenu() 161 "toggleRemoteDebugging" -> { 162 toggleRemoteJSDebugging() 163 // Reload the app after toggling debugging, this is based on what we do in DevSupportManagerBase. 164 reloadExpoApp() 165 } 166 "toggleElementInspector" -> toggleElementInspector() 167 "togglePerformanceMonitor" -> togglePerformanceMonitor() 168 } 169 } 170 } 171 } 172 173 // These commands (reload and devMenu) are here to match RN dev tooling. 174 175 // Reload the app on "reload" 176 packagerCommandHandlers["reload"] = object : NotificationOnlyHandler() { 177 override fun onNotification(params: Any?) { 178 reloadExpoApp() 179 } 180 } 181 182 // Open the dev menu on "devMenu" 183 packagerCommandHandlers["devMenu"] = object : NotificationOnlyHandler() { 184 override fun onNotification(params: Any?) { 185 toggleExpoDevMenu() 186 } 187 } 188 189 return packagerCommandHandlers 190 } 191 192 @JvmStatic fun getReactInstanceManagerBuilder(instanceManagerBuilderProperties: InstanceManagerBuilderProperties): ReactInstanceManagerBuilder { 193 // Build the instance manager 194 var builder = ReactInstanceManager.builder() 195 .setApplication(instanceManagerBuilderProperties.application) 196 .setJSIModulesPackage { reactApplicationContext: ReactApplicationContext, jsContext: JavaScriptContextHolder? -> 197 val devSupportManager = getDevSupportManager(reactApplicationContext) 198 if (devSupportManager == null) { 199 Log.e( 200 "Exponent", 201 "Couldn't get the `DevSupportManager`. JSI modules won't be initialized." 202 ) 203 return@setJSIModulesPackage emptyList() 204 } 205 val devSettings = devSupportManager.callRecursive("getDevSettings") 206 val isRemoteJSDebugEnabled = devSettings != null && devSettings.call("isRemoteJSDebugEnabled") as Boolean 207 if (!isRemoteJSDebugEnabled) { 208 return@setJSIModulesPackage ReanimatedJSIModulePackage().getJSIModules( 209 reactApplicationContext, 210 jsContext 211 ) 212 } 213 emptyList() 214 } 215 .addPackage(MainReactPackage()) 216 .addPackage( 217 ExponentPackage( 218 instanceManagerBuilderProperties.experienceProperties, 219 instanceManagerBuilderProperties.manifest, 220 // DO NOT EDIT THIS COMMENT - used by versioning scripts 221 // When distributing change the following two arguments to nulls 222 instanceManagerBuilderProperties.expoPackages, 223 instanceManagerBuilderProperties.exponentPackageDelegate, 224 instanceManagerBuilderProperties.singletonModules 225 ) 226 ) 227 .addPackage( 228 ExpoTurboPackage( 229 instanceManagerBuilderProperties.experienceProperties, 230 instanceManagerBuilderProperties.manifest 231 ) 232 ) 233 .setMinNumShakes(100) // disable the RN dev menu 234 .setInitialLifecycleState(LifecycleState.BEFORE_CREATE) 235 .setCustomPackagerCommandHandlers(createPackagerCommandHelpers()) 236 .setJavaScriptExecutorFactory(createJSExecutorFactory(instanceManagerBuilderProperties)) 237 if (instanceManagerBuilderProperties.jsBundlePath != null && instanceManagerBuilderProperties.jsBundlePath!!.isNotEmpty()) { 238 builder = builder.setJSBundleFile(instanceManagerBuilderProperties.jsBundlePath) 239 } 240 return builder 241 } 242 243 private fun getDevSupportManager(reactApplicationContext: ReactApplicationContext): RNObject? { 244 val currentActivity = Exponent.instance.currentActivity 245 return if (currentActivity != null) { 246 if (currentActivity is ReactNativeActivity) { 247 currentActivity.devSupportManager 248 } else { 249 null 250 } 251 } else try { 252 val devSettingsModule = reactApplicationContext.catalystInstance.getNativeModule("DevSettings") 253 val devSupportManagerField = devSettingsModule!!.javaClass.getDeclaredField("mDevSupportManager") 254 devSupportManagerField.isAccessible = true 255 RNObject.wrap(devSupportManagerField[devSettingsModule]!!) 256 } catch (e: Throwable) { 257 e.printStackTrace() 258 null 259 } 260 } 261 262 private fun createJSExecutorFactory( 263 instanceManagerBuilderProperties: InstanceManagerBuilderProperties 264 ): JavaScriptExecutorFactory? { 265 val appName = instanceManagerBuilderProperties.manifest.getName() ?: "" 266 val deviceName = AndroidInfoHelpers.getFriendlyDeviceName() 267 268 val jsEngineFromManifest = instanceManagerBuilderProperties.manifest.jsEngine 269 return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory( 270 appName, 271 deviceName 272 ) 273 } 274 } 275