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       reactInstanceManager.onHostPause()
291       // TODO: use onHostPause(activity)
292     }
293   }
294 
295   override fun onResume() {
296     super.onResume()
297     if (reactInstanceManager.isNotNull && !isCrashed) {
298       reactInstanceManager.onHostResume(this, this)
299     }
300   }
301 
302   override fun onDestroy() {
303     super.onDestroy()
304     destroyReactInstanceManager()
305     handler.removeCallbacksAndMessages(null)
306     EventBus.getDefault().unregister(this)
307   }
308 
309   public override fun onNewIntent(intent: Intent) {
310     if (reactInstanceManager.isNotNull && !isCrashed) {
311       try {
312         reactInstanceManager.call("onNewIntent", intent)
313       } catch (e: Throwable) {
314         EXL.e(TAG, e.toString())
315         super.onNewIntent(intent)
316       }
317     } else {
318       super.onNewIntent(intent)
319     }
320   }
321 
322   open val isDebugModeEnabled: Boolean
323     get() = manifest?.isDevelopmentMode() ?: false
324 
325   protected open fun destroyReactInstanceManager() {
326     if (reactInstanceManager.isNotNull && !isCrashed) {
327       reactInstanceManager.call("destroy")
328     }
329   }
330 
331   public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
332     super.onActivityResult(requestCode, resultCode, data)
333 
334     Exponent.instance.onActivityResult(requestCode, resultCode, data)
335 
336     if (reactInstanceManager.isNotNull && !isCrashed) {
337       reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data)
338     }
339 
340     // Have permission to draw over other apps. Resume loading.
341     if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) {
342       // startReactInstance() checks isInForeground and onActivityResult is called before onResume,
343       // so manually set this here.
344       isInForeground = true
345       startReactInstance()
346     }
347   }
348 
349   fun startReactInstance(
350     delegate: StartReactInstanceDelegate,
351     intentUri: String?,
352     sdkVersion: String?,
353     notification: ExponentNotification?,
354     isShellApp: Boolean,
355     extraNativeModules: List<Any>?,
356     extraExpoPackages: List<Package>?,
357     progressListener: DevBundleDownloadProgressListener
358   ): RNObject {
359     if (isCrashed || !delegate.isInForeground) {
360       // Can sometimes get here after an error has occurred. Return early or else we'll hit
361       // a null pointer at mReactRootView.startReactApplication
362       return RNObject("com.facebook.react.ReactInstanceManager")
363     }
364 
365     val experienceProperties = mapOf<String, Any?>(
366       KernelConstants.MANIFEST_URL_KEY to manifestUrl,
367       KernelConstants.LINKING_URI_KEY to linkingUri,
368       KernelConstants.INTENT_URI_KEY to intentUri,
369       KernelConstants.IS_HEADLESS_KEY to false
370     )
371 
372     val instanceManagerBuilderProperties = InstanceManagerBuilderProperties(
373       application = application,
374       jsBundlePath = jsBundlePath,
375       experienceProperties = experienceProperties,
376       expoPackages = extraExpoPackages,
377       exponentPackageDelegate = delegate.exponentPackageDelegate,
378       manifest = manifest!!,
379       singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages)
380     )
381 
382     val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!)
383     val builder = versionedUtils.callRecursive(
384       "getReactInstanceManagerBuilder",
385       instanceManagerBuilderProperties
386     )!!
387 
388     builder.call("setCurrentActivity", this)
389 
390     // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here
391     builder.call(
392       "setInitialLifecycleState",
393       RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED")
394     )
395 
396     if (extraNativeModules != null) {
397       for (nativeModule in extraNativeModules) {
398         builder.call("addPackage", nativeModule)
399       }
400     }
401 
402     if (delegate.isDebugModeEnabled) {
403       val debuggerHost = manifest!!.getDebuggerHost()
404       val mainModuleName = manifest!!.getMainModuleName()
405       Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder)
406 
407       val devLoadingView =
408         RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion)
409       devLoadingView.callRecursive("setDevLoadingEnabled", false)
410 
411       val devBundleDownloadListener =
412         RNObject("host.exp.exponent.ExponentDevBundleDownloadListener")
413           .loadVersion(sdkVersion)
414           .construct(progressListener)
415       builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get())
416     } else {
417       waitForReactAndFinishLoading()
418     }
419 
420     val bundle = Bundle()
421     val exponentProps = JSONObject()
422     if (notification != null) {
423       bundle.putString("notification", notification.body) // Deprecated
424       try {
425         exponentProps.put("notification", notification.toJSONObject("selected"))
426       } catch (e: JSONException) {
427         e.printStackTrace()
428       }
429     }
430 
431     try {
432       exponentProps.put("manifestString", manifest.toString())
433       exponentProps.put("shell", isShellApp)
434       exponentProps.put("initialUri", intentUri)
435     } catch (e: JSONException) {
436       EXL.e(TAG, e)
437     }
438 
439     val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!)
440     if (metadata != null) {
441       // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience,
442       // we need to send them with the standard notification events so that you can get all the unread notification through an event
443       // Copy unreadNotifications into exponentProps
444       if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) {
445         try {
446           val unreadNotifications =
447             metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
448           delegate.handleUnreadNotifications(unreadNotifications)
449         } catch (e: JSONException) {
450           e.printStackTrace()
451         }
452         metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
453       }
454       exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata)
455     }
456 
457     try {
458       bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps))
459     } catch (e: JSONException) {
460       throw Error("JSONObject failed to be converted to Bundle", e)
461     }
462 
463     if (!delegate.isInForeground) {
464       return RNObject("com.facebook.react.ReactInstanceManager")
465     }
466 
467     val mReactInstanceManager = builder.callRecursive("build")!!
468     val devSettings =
469       mReactInstanceManager.callRecursive("getDevSupportManager")!!.callRecursive("getDevSettings")
470     if (devSettings != null) {
471       devSettings.setField("exponentActivityId", activityId)
472       if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) {
473         if (manifest?.jsEngine == "hermes") {
474           // Disable remote debugging when running on Hermes
475           devSettings.call("setRemoteJSDebugEnabled", false)
476         }
477         waitForReactAndFinishLoading()
478       }
479     }
480 
481     mReactInstanceManager.onHostResume(this, this)
482     val appKey = manifest!!.getAppKey()
483     reactRootView.call(
484       "startReactApplication",
485       mReactInstanceManager.get(),
486       appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY,
487       initialProps(bundle)
488     )
489 
490     // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager}
491     // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}.
492     // Originally react-native will automatically attach after `startReactApplication`.
493     // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8,
494     // the only remaining path is by `onMeasure`.
495     reactRootView.call("requestLayout")
496 
497     return mReactInstanceManager
498   }
499 
500   protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean {
501     if (isLoading) {
502       // Don't hit ErrorRecoveryManager until bridge is initialized.
503       // This is the same on iOS.
504       return true
505     }
506     val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!)
507     errorRecoveryManager.markErrored()
508 
509     if (!errorRecoveryManager.shouldReloadOnError()) {
510       return true
511     }
512 
513     if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) {
514       // Kernel couldn't reload, show error screen
515       return true
516     }
517 
518     errorQueue.clear()
519 
520     return false
521   }
522 
523   fun onEventMainThread(event: AddedExperienceEventEvent) {
524     if (manifestUrl != null && manifestUrl == event.manifestUrl) {
525       pollForEventsToSendToRN()
526     }
527   }
528 
529   fun onEvent(event: ExperienceContentLoaded?) {}
530 
531   private fun pollForEventsToSendToRN() {
532     if (manifestUrl == null) {
533       return
534     }
535 
536     try {
537       val rctDeviceEventEmitter =
538         RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
539       rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
540       val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
541         .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())
542       if (existingEmitter != null) {
543         val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!)
544         for ((eventName, eventPayload) in events) {
545           existingEmitter.call("emit", eventName, eventPayload)
546         }
547       }
548     } catch (e: Throwable) {
549       EXL.e(TAG, e)
550     }
551   }
552 
553   /**
554    * Emits events to `RCTNativeAppEventEmitter`
555    */
556   fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) {
557     try {
558       val nativeAppEventEmitter =
559         RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter")
560       nativeAppEventEmitter.loadVersion(detachSdkVersion!!)
561       val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
562         .callRecursive("getJSModule", nativeAppEventEmitter.rnClass())
563       emitter?.call("emit", eventName, eventArgs)
564     } catch (e: Throwable) {
565       EXL.e(TAG, e)
566     }
567   }
568 
569   // for getting global permission
570   override fun checkSelfPermission(permission: String): Int {
571     return super.checkPermission(permission, Process.myPid(), Process.myUid())
572   }
573 
574   override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
575     // in scoped application we don't have `don't ask again` button
576     return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
577       true
578     } else super.shouldShowRequestPermissionRationale(permission)
579   }
580 
581   override fun requestPermissions(
582     permissions: Array<String>,
583     requestCode: Int,
584     listener: PermissionListener
585   ) {
586     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
587       val name = manifest!!.getName()
588       scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!)
589       scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener)
590     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
591       super.requestPermissions(permissions, requestCode)
592     }
593   }
594 
595   override fun onRequestPermissionsResult(
596     requestCode: Int,
597     permissions: Array<String>,
598     grantResults: IntArray
599   ) {
600     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
601       if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) {
602         if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) {
603           scopedPermissionsRequester = null
604         }
605       }
606     } else {
607       super.onRequestPermissionsResult(requestCode, permissions, grantResults)
608     }
609   }
610 
611   // for getting scoped permission
612   override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
613     val globalResult = super.checkPermission(permission, pid, uid)
614     return expoKernelServiceRegistry.permissionsKernelService.getPermissions(
615       globalResult,
616       packageManager,
617       permission,
618       experienceKey!!
619     )
620   }
621 
622   val devSupportManager: RNObject?
623     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager")
624 
625   val jsExecutorName: String?
626     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String
627 
628   // deprecated in favor of Expo.Linking.makeUrl
629   // TODO: remove this
630   private val linkingUri: String?
631     get() = if (Constants.SHELL_APP_SCHEME != null) {
632       Constants.SHELL_APP_SCHEME + "://"
633     } else {
634       val uri = Uri.parse(manifestUrl)
635       val host = uri.host
636       if (host != null && (
637         host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" ||
638           host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith(
639             ".expo.test"
640           )
641         )
642       ) {
643         val pathSegments = uri.pathSegments
644         val builder = uri.buildUpon()
645         builder.path(null)
646         for (segment in pathSegments) {
647           if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) {
648             break
649           }
650           builder.appendEncodedPath(segment)
651         }
652         builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build()
653           .toString()
654       } else {
655         manifestUrl
656       }
657     }
658 
659   companion object {
660     private val TAG = ReactNativeActivity::class.java.simpleName
661     private const val VIEW_TEST_INTERVAL_MS: Long = 20
662     @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList()
663   }
664 }
665