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