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