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