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