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         waitForReactAndFinishLoading()
476       }
477     }
478 
479     mReactInstanceManager.onHostResume(this, this)
480     val appKey = manifest!!.getAppKey()
481     reactRootView.call(
482       "startReactApplication",
483       mReactInstanceManager.get(),
484       appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY,
485       initialProps(bundle)
486     )
487 
488     // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager}
489     // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}.
490     // Originally react-native will automatically attach after `startReactApplication`.
491     // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8,
492     // the only remaining path is by `onMeasure`.
493     reactRootView.call("requestLayout")
494 
495     return mReactInstanceManager
496   }
497 
498   protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean {
499     if (isLoading) {
500       // Don't hit ErrorRecoveryManager until bridge is initialized.
501       // This is the same on iOS.
502       return true
503     }
504     val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!)
505     errorRecoveryManager.markErrored()
506 
507     if (!errorRecoveryManager.shouldReloadOnError()) {
508       return true
509     }
510 
511     if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) {
512       // Kernel couldn't reload, show error screen
513       return true
514     }
515 
516     errorQueue.clear()
517     try {
518       val eventProperties = JSONObject().apply {
519         put(Analytics.USER_ERROR_MESSAGE, errorMessage.userErrorMessage())
520         put(Analytics.DEVELOPER_ERROR_MESSAGE, errorMessage.developerErrorMessage())
521         put(Analytics.MANIFEST_URL, manifestUrl)
522       }
523       Analytics.logEvent(Analytics.AnalyticsEvent.ERROR_RELOADED, eventProperties)
524     } catch (e: Exception) {
525       EXL.e(TAG, e.message)
526     }
527 
528     return false
529   }
530 
531   fun onEventMainThread(event: AddedExperienceEventEvent) {
532     if (manifestUrl != null && manifestUrl == event.manifestUrl) {
533       pollForEventsToSendToRN()
534     }
535   }
536 
537   fun onEvent(event: ExperienceContentLoaded?) {}
538 
539   private fun pollForEventsToSendToRN() {
540     if (manifestUrl == null) {
541       return
542     }
543 
544     try {
545       val rctDeviceEventEmitter =
546         RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
547       rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
548       val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
549         .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())
550       if (existingEmitter != null) {
551         val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!)
552         for ((eventName, eventPayload) in events) {
553           existingEmitter.call("emit", eventName, eventPayload)
554         }
555       }
556     } catch (e: Throwable) {
557       EXL.e(TAG, e)
558     }
559   }
560 
561   // for getting global permission
562   override fun checkSelfPermission(permission: String): Int {
563     return super.checkPermission(permission, Process.myPid(), Process.myUid())
564   }
565 
566   override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
567     // in scoped application we don't have `don't ask again` button
568     return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
569       true
570     } else super.shouldShowRequestPermissionRationale(permission)
571   }
572 
573   override fun requestPermissions(
574     permissions: Array<String>,
575     requestCode: Int,
576     listener: PermissionListener
577   ) {
578     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
579       val name = manifest!!.getName()
580       scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!)
581       scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener)
582     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
583       super.requestPermissions(permissions, requestCode)
584     }
585   }
586 
587   override fun onRequestPermissionsResult(
588     requestCode: Int,
589     permissions: Array<String>,
590     grantResults: IntArray
591   ) {
592     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
593       if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) {
594         if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) {
595           scopedPermissionsRequester = null
596         }
597       }
598     } else {
599       super.onRequestPermissionsResult(requestCode, permissions, grantResults)
600     }
601   }
602 
603   // for getting scoped permission
604   override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
605     val globalResult = super.checkPermission(permission, pid, uid)
606     return expoKernelServiceRegistry.permissionsKernelService.getPermissions(
607       globalResult,
608       packageManager,
609       permission,
610       experienceKey!!
611     )
612   }
613 
614   val devSupportManager: RNObject?
615     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager")
616 
617   val jsExecutorName: String?
618     get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String
619 
620   // deprecated in favor of Expo.Linking.makeUrl
621   // TODO: remove this
622   private val linkingUri: String?
623     get() = if (Constants.SHELL_APP_SCHEME != null) {
624       Constants.SHELL_APP_SCHEME + "://"
625     } else {
626       val uri = Uri.parse(manifestUrl)
627       val host = uri.host
628       if (host != null && (
629         host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" ||
630           host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith(
631             ".expo.test"
632           )
633         )
634       ) {
635         val pathSegments = uri.pathSegments
636         val builder = uri.buildUpon()
637         builder.path(null)
638         for (segment in pathSegments) {
639           if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) {
640             break
641           }
642           builder.appendEncodedPath(segment)
643         }
644         builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build()
645           .toString()
646       } else {
647         manifestUrl
648       }
649     }
650 
651   companion object {
652     private val TAG = ReactNativeActivity::class.java.simpleName
653     private const val VIEW_TEST_INTERVAL_MS: Long = 20
654     @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList()
655   }
656 }
657