1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.experience
3 
4 import android.app.Activity
5 import android.content.Intent
6 import android.content.pm.PackageManager
7 import android.graphics.Color
8 import android.net.Uri
9 import android.os.Build
10 import android.os.Bundle
11 import android.os.Handler
12 import android.os.Process
13 import android.view.KeyEvent
14 import android.view.View
15 import android.view.ViewGroup
16 import android.widget.FrameLayout
17 import androidx.annotation.UiThread
18 import androidx.appcompat.app.AppCompatActivity
19 import androidx.core.content.ContextCompat
20 import com.facebook.infer.annotation.Assertions
21 import com.facebook.react.devsupport.DoubleTapReloadRecognizer
22 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
23 import com.facebook.react.modules.core.PermissionAwareActivity
24 import com.facebook.react.modules.core.PermissionListener
25 import de.greenrobot.event.EventBus
26 import expo.modules.core.interfaces.Package
27 import expo.modules.manifests.core.Manifest
28 import host.exp.exponent.Constants
29 import host.exp.exponent.ExponentManifest
30 import host.exp.exponent.RNObject
31 import host.exp.exponent.analytics.EXL
32 import host.exp.exponent.di.NativeModuleDepsProvider
33 import host.exp.exponent.experience.BaseExperienceActivity.ExperienceContentLoaded
34 import host.exp.exponent.experience.splashscreen.LoadingView
35 import host.exp.exponent.kernel.*
36 import host.exp.exponent.kernel.KernelConstants.AddedExperienceEventEvent
37 import host.exp.exponent.kernel.services.ErrorRecoveryManager
38 import host.exp.exponent.kernel.services.ExpoKernelServiceRegistry
39 import host.exp.exponent.notifications.ExponentNotification
40 import host.exp.exponent.storage.ExponentSharedPreferences
41 import host.exp.exponent.utils.BundleJSONConverter
42 import host.exp.exponent.utils.ExperienceActivityUtils
43 import host.exp.exponent.utils.ScopedPermissionsRequester
44 import host.exp.expoview.Exponent
45 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties
46 import host.exp.expoview.Exponent.StartReactInstanceDelegate
47 import host.exp.expoview.R
48 import org.json.JSONException
49 import org.json.JSONObject
50 import versioned.host.exp.exponent.ExponentPackage
51 import java.util.*
52 import javax.inject.Inject
53 
54 abstract class ReactNativeActivity :
55   AppCompatActivity(),
56   DefaultHardwareBackBtnHandler,
57   PermissionAwareActivity {
58 
59   class ExperienceDoneLoadingEvent internal constructor(val activity: Activity)
60 
61   open fun initialProps(expBundle: Bundle?): Bundle? {
62     return expBundle
63   }
64 
65   protected open fun onDoneLoading() {}
66 
67   // Will be called after waitForDrawOverOtherAppPermission
68   protected open fun startReactInstance() {}
69 
70   protected var reactInstanceManager: RNObject =
71     RNObject("com.facebook.react.ReactInstanceManager")
72   protected var isCrashed = false
73 
74   protected val networkInterceptor =
75     RNObject("host.exp.exponent.ExpoNetworkInterceptor")
76 
77   protected var manifestUrl: String? = null
78   var experienceKey: ExperienceKey? = null
79   protected var sdkVersion: String? = null
80   protected var activityId = 0
81 
82   // In detach we want UNVERSIONED most places. We still need the numbered sdk version
83   // when creating cache keys.
84   protected var detachSdkVersion: String? = null
85 
86   protected lateinit var reactRootView: RNObject
87   private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
88   var isLoading = true
89     protected set
90   protected var jsBundlePath: String? = null
91   protected var manifest: Manifest? = null
92   var isInForeground = false
93     protected set
94   private var scopedPermissionsRequester: ScopedPermissionsRequester? = null
95 
96   @Inject
97   protected lateinit var exponentSharedPreferences: ExponentSharedPreferences
98 
99   @Inject
100   lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry
101 
102   private lateinit var containerView: FrameLayout
103 
104   /**
105    * This view is optional and available only when the app runs in Expo Go.
106    */
107   private var loadingView: LoadingView? = null
108   private lateinit var reactContainerView: FrameLayout
109   private val handler = Handler()
110 
111   protected open fun shouldCreateLoadingView(): Boolean {
112     return !Constants.isStandaloneApp() || Constants.SHOW_LOADING_VIEW_IN_SHELL_APP
113   }
114 
115   val rootView: View?
116     get() = reactRootView.get() as View?
117 
118   override fun onCreate(savedInstanceState: Bundle?) {
119     super.onCreate(null)
120 
121     containerView = FrameLayout(this)
122     setContentView(containerView)
123 
124     reactContainerView = FrameLayout(this)
125     containerView.addView(reactContainerView)
126 
127     if (shouldCreateLoadingView()) {
128       containerView.setBackgroundColor(
129         ContextCompat.getColor(
130           this,
131           R.color.splashscreen_background
132         )
133       )
134       loadingView = LoadingView(this)
135       loadingView!!.show()
136       containerView.addView(loadingView)
137     }
138 
139     doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
140     Exponent.initialize(this, application)
141     NativeModuleDepsProvider.instance.inject(ReactNativeActivity::class.java, this)
142 
143     // Can't call this here because subclasses need to do other initialization
144     // before their listener methods are called.
145     // EventBus.getDefault().registerSticky(this);
146   }
147 
148   protected fun setReactRootView(reactRootView: View) {
149     reactContainerView.removeAllViews()
150     addReactViewToContentContainer(reactRootView)
151   }
152 
153   fun addReactViewToContentContainer(reactView: View) {
154     if (reactView.parent != null) {
155       (reactView.parent as ViewGroup).removeView(reactView)
156     }
157     reactContainerView.addView(reactView)
158   }
159 
160   fun hasReactView(reactView: View): Boolean {
161     return reactView.parent === reactContainerView
162   }
163 
164   protected fun hideLoadingView() {
165     loadingView?.let {
166       val viewGroup = it.parent as ViewGroup?
167       viewGroup?.removeView(it)
168       it.hide()
169     }
170     loadingView = null
171   }
172 
173   protected fun removeAllViewsFromContainer() {
174     containerView.removeAllViews()
175   }
176   // region Loading
177   /**
178    * Successfully finished loading
179    */
180   @UiThread
181   protected fun finishLoading() {
182     waitForReactAndFinishLoading()
183   }
184 
185   /**
186    * There was an error during loading phase
187    */
188   protected fun interruptLoading() {
189     handler.removeCallbacksAndMessages(null)
190   }
191 
192   // Loop until a view is added to the ReactRootView and once it happens run callback
193   private fun waitForReactRootViewToHaveChildrenAndRunCallback(callback: Runnable) {
194     if (reactRootView.isNull) {
195       return
196     }
197 
198     if (reactRootView.call("getChildCount") as Int > 0) {
199       callback.run()
200     } else {
201       handler.postDelayed(
202         { waitForReactRootViewToHaveChildrenAndRunCallback(callback) },
203         VIEW_TEST_INTERVAL_MS
204       )
205     }
206   }
207 
208   /**
209    * Waits for JS side of React to be launched and then performs final launching actions.
210    */
211   private fun waitForReactAndFinishLoading() {
212     if (Constants.isStandaloneApp() && Constants.SHOW_LOADING_VIEW_IN_SHELL_APP) {
213       val layoutParams = containerView.layoutParams
214       layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
215       containerView.layoutParams = layoutParams
216     }
217 
218     try {
219       // NOTE(evanbacon): Use the same view as the `expo-system-ui` module.
220       // Set before the application code runs to ensure immediate SystemUI calls overwrite the app.json value.
221       var rootView = this.window.decorView
222       ExperienceActivityUtils.setRootViewBackgroundColor(manifest!!, rootView)
223     } catch (e: Exception) {
224       EXL.e(TAG, e)
225     }
226 
227     waitForReactRootViewToHaveChildrenAndRunCallback {
228       onDoneLoading()
229       try {
230         // NOTE(evanbacon): The hierarchy at this point looks like:
231         // window.decorView > [4 other views] > containerView > reactContainerView > rootView > [RN App]
232         // This can be inspected using Android Studio: View > Tool Windows > Layout Inspector.
233         // Container background color is set for "loading" view state, we need to set it to transparent to prevent obstructing the root view.
234         containerView!!.setBackgroundColor(Color.TRANSPARENT)
235       } catch (e: Exception) {
236         EXL.e(TAG, e)
237       }
238       ErrorRecoveryManager.getInstance(experienceKey!!).markExperienceLoaded()
239       pollForEventsToSendToRN()
240       EventBus.getDefault().post(ExperienceDoneLoadingEvent(this))
241       isLoading = false
242     }
243   }
244   // endregion
245   // region SplashScreen
246   /**
247    * Get what version (among versioned classes) of ReactRootView.class SplashScreen module should be looking for.
248    */
249   protected fun getRootViewClass(manifest: Manifest): Class<out ViewGroup> {
250     val reactRootViewRNClass = reactRootView.rnClass()
251     if (reactRootViewRNClass != null) {
252       return reactRootViewRNClass as Class<out ViewGroup>
253     }
254     var sdkVersion = manifest.getExpoGoSDKVersion()
255     if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == this.sdkVersion) {
256       sdkVersion = RNObject.UNVERSIONED
257     }
258     sdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion
259     return RNObject("com.facebook.react.ReactRootView").loadVersion(sdkVersion!!).rnClass() as Class<out ViewGroup>
260   }
261 
262   // endregion
263   override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
264     devSupportManager?.let { devSupportManager ->
265       if (!isCrashed && devSupportManager.call("getDevSupportEnabled") as Boolean) {
266         val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer)
267           .didDoubleTapR(keyCode, currentFocus)
268         if (didDoubleTapR) {
269           devSupportManager.call("reloadExpoApp")
270           return true
271         }
272       }
273     }
274 
275     return super.onKeyUp(keyCode, event)
276   }
277 
278   override fun onBackPressed() {
279     if (reactInstanceManager.isNotNull && !isCrashed) {
280       reactInstanceManager.call("onBackPressed")
281     } else {
282       super.onBackPressed()
283     }
284   }
285 
286   override fun invokeDefaultOnBackPressed() {
287     super.onBackPressed()
288   }
289 
290   override fun onPause() {
291     super.onPause()
292     if (reactInstanceManager.isNotNull && !isCrashed) {
293       networkInterceptor.call("onPause")
294       reactInstanceManager.onHostPause()
295       // TODO: use onHostPause(activity)
296     }
297   }
298 
299   override fun onResume() {
300     super.onResume()
301     if (reactInstanceManager.isNotNull && !isCrashed) {
302       reactInstanceManager.onHostResume(this, this)
303       networkInterceptor.call("onResume", reactInstanceManager.get())
304     }
305   }
306 
307   override fun onDestroy() {
308     super.onDestroy()
309     destroyReactInstanceManager()
310     handler.removeCallbacksAndMessages(null)
311     EventBus.getDefault().unregister(this)
312   }
313 
314   public override fun onNewIntent(intent: Intent) {
315     if (reactInstanceManager.isNotNull && !isCrashed) {
316       try {
317         reactInstanceManager.call("onNewIntent", intent)
318       } catch (e: Throwable) {
319         EXL.e(TAG, e.toString())
320         super.onNewIntent(intent)
321       }
322     } else {
323       super.onNewIntent(intent)
324     }
325   }
326 
327   open val isDebugModeEnabled: Boolean
328     get() = manifest?.isDevelopmentMode() ?: false
329 
330   protected open fun destroyReactInstanceManager() {
331     if (reactInstanceManager.isNotNull && !isCrashed) {
332       reactInstanceManager.call("destroy")
333     }
334   }
335 
336   public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
337     super.onActivityResult(requestCode, resultCode, data)
338 
339     Exponent.instance.onActivityResult(requestCode, resultCode, data)
340 
341     if (reactInstanceManager.isNotNull && !isCrashed) {
342       reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data)
343     }
344 
345     // Have permission to draw over other apps. Resume loading.
346     if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) {
347       // startReactInstance() checks isInForeground and onActivityResult is called before onResume,
348       // so manually set this here.
349       isInForeground = true
350       startReactInstance()
351     }
352   }
353 
354   fun startReactInstance(
355     delegate: StartReactInstanceDelegate,
356     intentUri: String?,
357     sdkVersion: String?,
358     notification: ExponentNotification?,
359     isShellApp: Boolean,
360     extraNativeModules: List<Any>?,
361     extraExpoPackages: List<Package>?,
362     progressListener: DevBundleDownloadProgressListener
363   ): RNObject {
364     if (isCrashed || !delegate.isInForeground) {
365       // Can sometimes get here after an error has occurred. Return early or else we'll hit
366       // a null pointer at mReactRootView.startReactApplication
367       return RNObject("com.facebook.react.ReactInstanceManager")
368     }
369 
370     val experienceProperties = mapOf<String, Any?>(
371       KernelConstants.MANIFEST_URL_KEY to manifestUrl,
372       KernelConstants.LINKING_URI_KEY to linkingUri,
373       KernelConstants.INTENT_URI_KEY to intentUri,
374       KernelConstants.IS_HEADLESS_KEY to false
375     )
376 
377     val instanceManagerBuilderProperties = InstanceManagerBuilderProperties(
378       application = application,
379       jsBundlePath = jsBundlePath,
380       experienceProperties = experienceProperties,
381       expoPackages = extraExpoPackages,
382       exponentPackageDelegate = delegate.exponentPackageDelegate,
383       manifest = manifest!!,
384       singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages)
385     )
386 
387     val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!)
388     val builder = versionedUtils.callRecursive(
389       "getReactInstanceManagerBuilder",
390       instanceManagerBuilderProperties
391     )!!
392 
393     builder.call("setCurrentActivity", this)
394 
395     // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here
396     builder.call(
397       "setInitialLifecycleState",
398       RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED")
399     )
400 
401     if (extraNativeModules != null) {
402       for (nativeModule in extraNativeModules) {
403         builder.call("addPackage", nativeModule)
404       }
405     }
406 
407     if (delegate.isDebugModeEnabled) {
408       val debuggerHost = manifest!!.getDebuggerHost()
409       val mainModuleName = manifest!!.getMainModuleName()
410       Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder)
411 
412       val devLoadingView =
413         RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion)
414       devLoadingView.callRecursive("setDevLoadingEnabled", false)
415 
416       val devBundleDownloadListener =
417         RNObject("host.exp.exponent.ExponentDevBundleDownloadListener")
418           .loadVersion(sdkVersion)
419           .construct(progressListener)
420       builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get())
421     } else {
422       waitForReactAndFinishLoading()
423     }
424 
425     val bundle = Bundle()
426     val exponentProps = JSONObject()
427     if (notification != null) {
428       bundle.putString("notification", notification.body) // Deprecated
429       try {
430         exponentProps.put("notification", notification.toJSONObject("selected"))
431       } catch (e: JSONException) {
432         e.printStackTrace()
433       }
434     }
435 
436     try {
437       exponentProps.put("manifestString", manifest.toString())
438       exponentProps.put("shell", isShellApp)
439       exponentProps.put("initialUri", intentUri)
440     } catch (e: JSONException) {
441       EXL.e(TAG, e)
442     }
443 
444     val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!)
445     if (metadata != null) {
446       // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience,
447       // we need to send them with the standard notification events so that you can get all the unread notification through an event
448       // Copy unreadNotifications into exponentProps
449       if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) {
450         try {
451           val unreadNotifications =
452             metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
453           delegate.handleUnreadNotifications(unreadNotifications)
454         } catch (e: JSONException) {
455           e.printStackTrace()
456         }
457         metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
458       }
459       exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata)
460     }
461 
462     try {
463       bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps))
464     } catch (e: JSONException) {
465       throw Error("JSONObject failed to be converted to Bundle", e)
466     }
467 
468     if (!delegate.isInForeground) {
469       return RNObject("com.facebook.react.ReactInstanceManager")
470     }
471 
472     val mReactInstanceManager = builder.callRecursive("build")!!
473     val devSettings =
474       mReactInstanceManager.callRecursive("getDevSupportManager")!!.callRecursive("getDevSettings")
475     if (devSettings != null) {
476       devSettings.setField("exponentActivityId", activityId)
477       if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) {
478         if (manifest?.jsEngine == "hermes") {
479           // Disable remote debugging when running on Hermes
480           devSettings.call("setRemoteJSDebugEnabled", false)
481         }
482         waitForReactAndFinishLoading()
483       }
484     }
485 
486     mReactInstanceManager.onHostResume(this, this)
487     val appKey = manifest!!.getAppKey()
488     reactRootView.call(
489       "startReactApplication",
490       mReactInstanceManager.get(),
491       appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY,
492       initialProps(bundle)
493     )
494 
495     networkInterceptor.loadVersion(sdkVersion).construct().call("start", manifest, mReactInstanceManager.get())
496 
497     // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager}
498     // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}.
499     // Originally react-native will automatically attach after `startReactApplication`.
500     // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8,
501     // the only remaining path is by `onMeasure`.
502     reactRootView.call("requestLayout")
503 
504     return mReactInstanceManager
505   }
506 
507   protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean {
508     if (isLoading) {
509       // Don't hit ErrorRecoveryManager until bridge is initialized.
510       // This is the same on iOS.
511       return true
512     }
513     val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!)
514     errorRecoveryManager.markErrored()
515 
516     if (!errorRecoveryManager.shouldReloadOnError()) {
517       return true
518     }
519 
520     if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) {
521       // Kernel couldn't reload, show error screen
522       return true
523     }
524 
525     errorQueue.clear()
526 
527     return false
528   }
529 
530   fun onEventMainThread(event: AddedExperienceEventEvent) {
531     if (manifestUrl != null && manifestUrl == event.manifestUrl) {
532       pollForEventsToSendToRN()
533     }
534   }
535 
536   fun onEvent(event: ExperienceContentLoaded?) {}
537 
538   private fun pollForEventsToSendToRN() {
539     if (manifestUrl == null) {
540       return
541     }
542 
543     try {
544       val rctDeviceEventEmitter =
545         RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
546       rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
547       val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
548         .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())
549       if (existingEmitter != null) {
550         val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!)
551         for ((eventName, eventPayload) in events) {
552           existingEmitter.call("emit", eventName, eventPayload)
553         }
554       }
555     } catch (e: Throwable) {
556       EXL.e(TAG, e)
557     }
558   }
559 
560   /**
561    * Emits events to `RCTNativeAppEventEmitter`
562    */
563   fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) {
564     try {
565       val nativeAppEventEmitter =
566         RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter")
567       nativeAppEventEmitter.loadVersion(detachSdkVersion!!)
568       val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
569         .callRecursive("getJSModule", nativeAppEventEmitter.rnClass())
570       emitter?.call("emit", eventName, eventArgs)
571     } catch (e: Throwable) {
572       EXL.e(TAG, e)
573     }
574   }
575 
576   // for getting global permission
577   override fun checkSelfPermission(permission: String): Int {
578     return super.checkPermission(permission, Process.myPid(), Process.myUid())
579   }
580 
581   override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
582     // in scoped application we don't have `don't ask again` button
583     return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
584       true
585     } else super.shouldShowRequestPermissionRationale(permission)
586   }
587 
588   override fun requestPermissions(
589     permissions: Array<String>,
590     requestCode: Int,
591     listener: PermissionListener
592   ) {
593     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
594       val name = manifest!!.getName()
595       scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!)
596       scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener)
597     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
598       super.requestPermissions(permissions, requestCode)
599     }
600   }
601 
602   override fun onRequestPermissionsResult(
603     requestCode: Int,
604     permissions: Array<String>,
605     grantResults: IntArray
606   ) {
607     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
608       if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) {
609         if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) {
610           scopedPermissionsRequester = null
611         }
612       }
613     } else {
614       super.onRequestPermissionsResult(requestCode, permissions, grantResults)
615     }
616   }
617 
618   // for getting scoped permission
619   override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
620     val globalResult = super.checkPermission(permission, pid, uid)
621     return expoKernelServiceRegistry.permissionsKernelService.getPermissions(
622       globalResult,
623       packageManager,
624       permission,
625       experienceKey!!
626     )
627   }
628 
629   val devSupportManager: RNObject?
630     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager")
631 
632   val jsExecutorName: String?
633     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String
634 
635   // deprecated in favor of Expo.Linking.makeUrl
636   // TODO: remove this
637   private val linkingUri: String?
638     get() = if (Constants.SHELL_APP_SCHEME != null) {
639       Constants.SHELL_APP_SCHEME + "://"
640     } else {
641       val uri = Uri.parse(manifestUrl)
642       val host = uri.host
643       if (host != null && (
644         host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" ||
645           host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith(
646             ".expo.test"
647           )
648         )
649       ) {
650         val pathSegments = uri.pathSegments
651         val builder = uri.buildUpon()
652         builder.path(null)
653         for (segment in pathSegments) {
654           if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) {
655             break
656           }
657           builder.appendEncodedPath(segment)
658         }
659         builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build()
660           .toString()
661       } else {
662         manifestUrl
663       }
664     }
665 
666   companion object {
667     private val TAG = ReactNativeActivity::class.java.simpleName
668     private const val VIEW_TEST_INTERVAL_MS: Long = 20
669     @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList()
670   }
671 }
672