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