1 package versioned.host.exp.exponent.modules.internal 2 3 import android.content.Intent 4 import android.net.Uri 5 import android.os.Build 6 import android.os.Bundle 7 import android.provider.Settings 8 import com.facebook.react.bridge.LifecycleEventListener 9 import com.facebook.react.bridge.ReactApplicationContext 10 import com.facebook.react.bridge.ReactContextBaseJavaModule 11 import com.facebook.react.bridge.UiThreadUtil 12 import com.facebook.react.devsupport.DevInternalSettings 13 import com.facebook.react.devsupport.BridgeDevSupportManager 14 import com.facebook.react.devsupport.HMRClient 15 import expo.modules.manifests.core.Manifest 16 import host.exp.exponent.di.NativeModuleDepsProvider 17 import host.exp.exponent.experience.ExperienceActivity 18 import host.exp.exponent.experience.ReactNativeActivity 19 import host.exp.exponent.kernel.DevMenuManager 20 import host.exp.exponent.kernel.DevMenuModuleInterface 21 import host.exp.exponent.kernel.KernelConstants 22 import host.exp.expoview.Exponent 23 import host.exp.expoview.R 24 import okhttp3.Request 25 import okhttp3.RequestBody.Companion.toRequestBody 26 import java.util.* 27 import javax.inject.Inject 28 29 class DevMenuModule(reactContext: ReactApplicationContext, val experienceProperties: Map<String, Any?>, val manifest: Manifest?) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener, DevMenuModuleInterface { 30 31 @Inject 32 internal lateinit var devMenuManager: DevMenuManager 33 34 init { 35 NativeModuleDepsProvider.instance.inject(DevMenuModule::class.java, this) 36 reactContext.addLifecycleEventListener(this) 37 } 38 39 //region publics 40 getNamenull41 override fun getName(): String = "ExpoDevMenu" 42 43 //endregion publics 44 //region DevMenuModuleInterface 45 46 /** 47 * Returns manifestUrl of the experience which can be used as its ID. 48 */ 49 override fun getManifestUrl(): String { 50 val manifestUrl = experienceProperties[KernelConstants.MANIFEST_URL_KEY] as String? 51 return manifestUrl ?: "" 52 } 53 54 /** 55 * Returns a [Bundle] containing initialProps that will be used to render the dev menu for related experience. 56 */ getInitialPropsnull57 override fun getInitialProps(): Bundle { 58 val bundle = Bundle() 59 val taskBundle = Bundle() 60 61 taskBundle.putString("manifestUrl", getManifestUrl()) 62 taskBundle.putString("manifestString", manifest?.toString()) 63 64 bundle.putBundle("task", taskBundle) 65 bundle.putString("uuid", UUID.randomUUID().toString()) 66 67 return bundle 68 } 69 70 /** 71 * Returns a [Bundle] with all available dev menu options for related experience. 72 */ getMenuItemsnull73 override fun getMenuItems(): Bundle { 74 val devSupportManager = getDevSupportManager() 75 val devSettings = devSupportManager?.devSettings 76 77 val items = Bundle() 78 val inspectorMap = Bundle() 79 val debuggerMap = Bundle() 80 val hmrMap = Bundle() 81 val perfMap = Bundle() 82 83 if (devSettings != null && devSupportManager.devSupportEnabled) { 84 inspectorMap.putString("label", getString(if (devSettings.isElementInspectorEnabled) R.string.devmenu_hide_element_inspector else R.string.devmenu_show_element_inspector)) 85 inspectorMap.putBoolean("isEnabled", true) 86 } else { 87 inspectorMap.putString("label", getString(R.string.devmenu_element_inspector_unavailable)) 88 inspectorMap.putBoolean("isEnabled", false) 89 } 90 items.putBundle("dev-inspector", inspectorMap) 91 92 if (devSettings != null && devSupportManager.devSupportEnabled && isJsExecutorInspectable) { 93 debuggerMap.putString("label", getString(R.string.devmenu_open_js_debugger)) 94 debuggerMap.putBoolean("isEnabled", devSupportManager.devSupportEnabled) 95 items.putBundle("dev-remote-debug", debuggerMap) 96 } else if (devSettings != null && devSupportManager.devSupportEnabled && manifest?.getExpoGoSDKVersion() ?: "" < "49.0.0") { 97 debuggerMap.putString("label", getString(if (devSettings.isRemoteJSDebugEnabled) R.string.devmenu_stop_remote_debugging else R.string.devmenu_start_remote_debugging)) 98 debuggerMap.putBoolean("isEnabled", devSupportManager.devSupportEnabled) 99 items.putBundle("dev-remote-debug", debuggerMap) 100 } 101 102 if (devSettings != null && devSupportManager.devSupportEnabled && devSettings is DevInternalSettings) { 103 hmrMap.putString("label", getString(if (devSettings.isHotModuleReplacementEnabled) R.string.devmenu_disable_fast_refresh else R.string.devmenu_enable_fast_refresh)) 104 hmrMap.putBoolean("isEnabled", true) 105 } else { 106 hmrMap.putString("label", getString(R.string.devmenu_fast_refresh_unavailable)) 107 hmrMap.putString("detail", getString(R.string.devmenu_fast_refresh_unavailable_details)) 108 hmrMap.putBoolean("isEnabled", false) 109 } 110 items.putBundle("dev-hmr", hmrMap) 111 112 if (devSettings != null && devSupportManager.devSupportEnabled) { 113 perfMap.putString("label", getString(if (devSettings.isFpsDebugEnabled) R.string.devmenu_hide_performance_monitor else R.string.devmenu_show_performance_monitor)) 114 perfMap.putBoolean("isEnabled", true) 115 } else { 116 perfMap.putString("label", getString(R.string.devmenu_performance_monitor_unavailable)) 117 perfMap.putBoolean("isEnabled", false) 118 } 119 items.putBundle("dev-perf-monitor", perfMap) 120 121 return items 122 } 123 124 /** 125 * Handles selecting dev menu options returned by [getMenuItems]. 126 */ selectItemWithKeynull127 override fun selectItemWithKey(itemKey: String) { 128 val devSupportManager = getDevSupportManager() 129 val devSettings = devSupportManager?.devSettings as DevInternalSettings? 130 131 if (devSupportManager == null || devSettings == null) { 132 return 133 } 134 135 UiThreadUtil.runOnUiThread { 136 when (itemKey) { 137 "dev-remote-debug" -> { 138 if (isJsExecutorInspectable) { 139 openJsInspector() 140 } else { 141 devSettings.isRemoteJSDebugEnabled = !devSettings.isRemoteJSDebugEnabled 142 devSupportManager.handleReloadJS() 143 } 144 } 145 "dev-hmr" -> { 146 val nextEnabled = !devSettings.isHotModuleReplacementEnabled 147 val hmrClient: HMRClient? = reactApplicationContext?.getJSModule(HMRClient::class.java) 148 149 devSettings.isHotModuleReplacementEnabled = nextEnabled 150 if (nextEnabled) hmrClient?.enable() else hmrClient?.disable() 151 } 152 "dev-inspector" -> devSupportManager.toggleElementInspector() 153 "dev-perf-monitor" -> { 154 if (!devSettings.isFpsDebugEnabled) { 155 // Request overlay permission if needed when "Show Perf Monitor" option is selected 156 requestOverlaysPermission() 157 } 158 devSupportManager.setFpsDebugEnabled(!devSettings.isFpsDebugEnabled) 159 } 160 } 161 } 162 } 163 164 /** 165 * Reloads JavaScript bundle without reloading the manifest. 166 */ reloadAppnull167 override fun reloadApp() { 168 getDevSupportManager()?.handleReloadJS() 169 } 170 171 /** 172 * Returns boolean value determining whether this app supports developer tools. 173 */ isDevSupportEnablednull174 override fun isDevSupportEnabled(): Boolean { 175 return manifest != null && manifest.isUsingDeveloperTool() 176 } 177 178 //endregion DevMenuModuleInterface 179 //region LifecycleEventListener 180 onHostResumenull181 override fun onHostResume() { 182 val activity = currentActivity 183 184 if (activity is ExperienceActivity) { 185 devMenuManager.registerDevMenuModuleForActivity(this, activity) 186 } 187 } 188 onHostPausenull189 override fun onHostPause() {} 190 onHostDestroynull191 override fun onHostDestroy() {} 192 193 //endregion LifecycleEventListener 194 //region internals 195 196 /** 197 * Returns versioned instance of [BridgeDevSupportManager], 198 * or null if no activity is currently attached to react context. 199 */ getDevSupportManagernull200 private fun getDevSupportManager(): BridgeDevSupportManager? { 201 val activity = currentActivity as? ReactNativeActivity? 202 return activity?.devSupportManager?.get() as? BridgeDevSupportManager? 203 } 204 205 /** 206 * Requests for the permission that allows the app to draw overlays on other apps. 207 * Such permission is required for example to enable performance monitor. 208 */ requestOverlaysPermissionnull209 private fun requestOverlaysPermission() { 210 val context = currentActivity ?: return 211 212 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 213 // Get permission to show debug overlay in dev builds. 214 if (!Settings.canDrawOverlays(context)) { 215 val intent = Intent( 216 Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 217 Uri.parse("package:" + context.packageName) 218 ) 219 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 220 if (intent.resolveActivity(context.packageManager) != null) { 221 context.startActivity(intent) 222 } 223 } 224 } 225 } 226 227 /** 228 * Helper for getting localized [String] from `strings.xml` file. 229 */ getStringnull230 private fun getString(ref: Int): String { 231 return reactApplicationContext.resources.getString(ref) 232 } 233 234 /** 235 * Indicates whether the underlying js executor supports inspecting. 236 * NOTE: because current react-native doesn't pass jsi runtime `isInspectable` to java, 237 * workaround to determine the state by executor name. 238 */ <lambda>null239 private val isJsExecutorInspectable: Boolean by lazy { 240 val activity = currentActivity as? ReactNativeActivity 241 activity?.jsExecutorName == "JSIExecutor+HermesRuntime" 242 } 243 244 /** 245 * Open the JavaScript inspector 246 */ openJsInspectornull247 private fun openJsInspector() { 248 reactApplicationContext.runOnNativeModulesQueueThread { 249 val devSupportManager = getDevSupportManager() 250 devSupportManager?.devSettings?.packagerConnectionSettings?.inspectorServerHost?.let { 251 val url = "http://$it/inspector?applicationId=${reactApplicationContext.packageName}" 252 val request = Request.Builder().url(url).put("".toRequestBody()).build() 253 Exponent.instance.exponentNetwork.noCacheClient.newCall(request).execute() 254 } 255 } 256 } 257 258 //endregion internals 259 } 260