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 createPackagerCommandHelpers(): Map<String, RequestHandler> { 148 // Attach listeners to the bundler's dev server web socket connection. 149 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 150 val packagerCommandHandlers = mutableMapOf<String, RequestHandler>() 151 152 // Enable a lot of tools under the same command namespace 153 packagerCommandHandlers["sendDevCommand"] = object : NotificationOnlyHandler() { 154 override fun onNotification(params: Any?) { 155 if (params != null && params is JSONObject) { 156 when (params.getNullable<String>("name")) { 157 "reload" -> reloadExpoApp() 158 "toggleDevMenu" -> toggleExpoDevMenu() 159 "toggleRemoteDebugging" -> { 160 toggleRemoteJSDebugging() 161 // Reload the app after toggling debugging, this is based on what we do in DevSupportManagerBase. 162 reloadExpoApp() 163 } 164 "toggleElementInspector" -> toggleElementInspector() 165 "togglePerformanceMonitor" -> togglePerformanceMonitor() 166 } 167 } 168 } 169 } 170 171 // These commands (reload and devMenu) are here to match RN dev tooling. 172 173 // Reload the app on "reload" 174 packagerCommandHandlers["reload"] = object : NotificationOnlyHandler() { 175 override fun onNotification(params: Any?) { 176 reloadExpoApp() 177 } 178 } 179 180 // Open the dev menu on "devMenu" 181 packagerCommandHandlers["devMenu"] = object : NotificationOnlyHandler() { 182 override fun onNotification(params: Any?) { 183 toggleExpoDevMenu() 184 } 185 } 186 187 return packagerCommandHandlers 188 } 189 190 @JvmStatic fun getReactInstanceManagerBuilder(instanceManagerBuilderProperties: InstanceManagerBuilderProperties): ReactInstanceManagerBuilder { 191 // Build the instance manager 192 var builder = ReactInstanceManager.builder() 193 .setApplication(instanceManagerBuilderProperties.application) 194 .setJSIModulesPackage { reactApplicationContext: ReactApplicationContext, jsContext: JavaScriptContextHolder? -> 195 emptyList() 196 } 197 .addPackage(MainReactPackage()) 198 .addPackage( 199 ExponentPackage( 200 instanceManagerBuilderProperties.experienceProperties, 201 instanceManagerBuilderProperties.manifest, 202 // DO NOT EDIT THIS COMMENT - used by versioning scripts 203 // When distributing change the following two arguments to nulls 204 instanceManagerBuilderProperties.expoPackages, 205 instanceManagerBuilderProperties.exponentPackageDelegate, 206 instanceManagerBuilderProperties.singletonModules 207 ) 208 ) 209 .addPackage( 210 ExpoTurboPackage( 211 instanceManagerBuilderProperties.experienceProperties, 212 instanceManagerBuilderProperties.manifest 213 ) 214 ) 215 .setMinNumShakes(100) // disable the RN dev menu 216 .setInitialLifecycleState(LifecycleState.BEFORE_CREATE) 217 .setCustomPackagerCommandHandlers(createPackagerCommandHelpers()) 218 .setJavaScriptExecutorFactory(createJSExecutorFactory(instanceManagerBuilderProperties)) 219 if (instanceManagerBuilderProperties.jsBundlePath != null && instanceManagerBuilderProperties.jsBundlePath!!.isNotEmpty()) { 220 builder = builder.setJSBundleFile(instanceManagerBuilderProperties.jsBundlePath) 221 } 222 return builder 223 } 224 225 private fun getDevSupportManager(reactApplicationContext: ReactApplicationContext): RNObject? { 226 val currentActivity = Exponent.instance.currentActivity 227 return if (currentActivity != null) { 228 if (currentActivity is ReactNativeActivity) { 229 currentActivity.devSupportManager 230 } else { 231 null 232 } 233 } else try { 234 val devSettingsModule = reactApplicationContext.catalystInstance.getNativeModule("DevSettings") 235 val devSupportManagerField = devSettingsModule!!.javaClass.getDeclaredField("mDevSupportManager") 236 devSupportManagerField.isAccessible = true 237 RNObject.wrap(devSupportManagerField[devSettingsModule]!!) 238 } catch (e: Throwable) { 239 e.printStackTrace() 240 null 241 } 242 } 243 244 private fun createJSExecutorFactory( 245 instanceManagerBuilderProperties: InstanceManagerBuilderProperties 246 ): JavaScriptExecutorFactory? { 247 val appName = instanceManagerBuilderProperties.manifest.getName() ?: "" 248 val deviceName = AndroidInfoHelpers.getFriendlyDeviceName() 249 250 val jsEngineFromManifest = instanceManagerBuilderProperties.manifest.jsEngine 251 return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory( 252 appName, 253 deviceName 254 ) 255 } 256 } 257