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