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