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