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