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.Bundle 10 import android.os.Handler 11 import android.os.Process 12 import android.view.KeyEvent 13 import android.view.View 14 import android.view.ViewGroup 15 import android.widget.FrameLayout 16 import androidx.annotation.UiThread 17 import androidx.appcompat.app.AppCompatActivity 18 import androidx.core.content.ContextCompat 19 import com.facebook.infer.annotation.Assertions 20 import com.facebook.react.devsupport.DoubleTapReloadRecognizer 21 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler 22 import com.facebook.react.modules.core.PermissionAwareActivity 23 import com.facebook.react.modules.core.PermissionListener 24 import de.greenrobot.event.EventBus 25 import expo.modules.core.interfaces.Package 26 import expo.modules.manifests.core.Manifest 27 import host.exp.exponent.Constants 28 import host.exp.exponent.ExponentManifest 29 import host.exp.exponent.RNObject 30 import host.exp.exponent.analytics.EXL 31 import host.exp.exponent.di.NativeModuleDepsProvider 32 import host.exp.exponent.experience.BaseExperienceActivity.ExperienceContentLoaded 33 import host.exp.exponent.experience.splashscreen.LoadingView 34 import host.exp.exponent.kernel.* 35 import host.exp.exponent.kernel.KernelConstants.AddedExperienceEventEvent 36 import host.exp.exponent.kernel.services.ErrorRecoveryManager 37 import host.exp.exponent.kernel.services.ExpoKernelServiceRegistry 38 import host.exp.exponent.notifications.ExponentNotification 39 import host.exp.exponent.storage.ExponentSharedPreferences 40 import host.exp.exponent.utils.BundleJSONConverter 41 import host.exp.exponent.utils.ExperienceActivityUtils 42 import host.exp.exponent.utils.ScopedPermissionsRequester 43 import host.exp.expoview.Exponent 44 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties 45 import host.exp.expoview.Exponent.StartReactInstanceDelegate 46 import host.exp.expoview.R 47 import org.json.JSONException 48 import org.json.JSONObject 49 import versioned.host.exp.exponent.ExponentPackage 50 import java.util.* 51 import javax.inject.Inject 52 53 abstract class ReactNativeActivity : 54 AppCompatActivity(), 55 DefaultHardwareBackBtnHandler, 56 PermissionAwareActivity { 57 58 class ExperienceDoneLoadingEvent internal constructor(val activity: Activity) 59 60 open fun initialProps(expBundle: Bundle?): Bundle? { 61 return expBundle 62 } 63 64 protected open fun onDoneLoading() {} 65 66 // Will be called after waitForDrawOverOtherAppPermission 67 protected open fun startReactInstance() {} 68 69 protected var reactInstanceManager: RNObject = 70 RNObject("com.facebook.react.ReactInstanceManager") 71 protected var isCrashed = false 72 73 protected var manifestUrl: String? = null 74 var experienceKey: ExperienceKey? = null 75 protected var sdkVersion: String? = null 76 protected var activityId = 0 77 78 // In detach we want UNVERSIONED most places. We still need the numbered sdk version 79 // when creating cache keys. 80 protected var detachSdkVersion: String? = null 81 82 protected lateinit var reactRootView: RNObject 83 private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer 84 var isLoading = true 85 protected set 86 protected var jsBundlePath: String? = null 87 protected var manifest: Manifest? = null 88 var isInForeground = false 89 protected set 90 private var scopedPermissionsRequester: ScopedPermissionsRequester? = null 91 92 @Inject 93 protected lateinit var exponentSharedPreferences: ExponentSharedPreferences 94 95 @Inject 96 lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry 97 98 private lateinit var containerView: FrameLayout 99 100 /** 101 * This view is optional and available only when the app runs in Expo Go. 102 */ 103 private var loadingView: LoadingView? = null 104 private lateinit var reactContainerView: FrameLayout 105 private val handler = Handler() 106 107 protected open fun shouldCreateLoadingView(): Boolean { 108 return !Constants.isStandaloneApp() || Constants.SHOW_LOADING_VIEW_IN_SHELL_APP 109 } 110 111 val rootView: View? 112 get() = reactRootView.get() as View? 113 114 override fun onCreate(savedInstanceState: Bundle?) { 115 super.onCreate(null) 116 117 containerView = FrameLayout(this) 118 setContentView(containerView) 119 120 reactContainerView = FrameLayout(this) 121 containerView.addView(reactContainerView) 122 123 if (shouldCreateLoadingView()) { 124 containerView.setBackgroundColor( 125 ContextCompat.getColor( 126 this, 127 R.color.splashscreen_background 128 ) 129 ) 130 loadingView = LoadingView(this) 131 loadingView!!.show() 132 containerView.addView(loadingView) 133 } 134 135 doubleTapReloadRecognizer = DoubleTapReloadRecognizer() 136 Exponent.initialize(this, application) 137 NativeModuleDepsProvider.instance.inject(ReactNativeActivity::class.java, this) 138 139 // Can't call this here because subclasses need to do other initialization 140 // before their listener methods are called. 141 // EventBus.getDefault().registerSticky(this); 142 } 143 144 protected fun setReactRootView(reactRootView: View) { 145 reactContainerView.removeAllViews() 146 addReactViewToContentContainer(reactRootView) 147 } 148 149 fun addReactViewToContentContainer(reactView: View) { 150 if (reactView.parent != null) { 151 (reactView.parent as ViewGroup).removeView(reactView) 152 } 153 reactContainerView.addView(reactView) 154 } 155 156 fun hasReactView(reactView: View): Boolean { 157 return reactView.parent === reactContainerView 158 } 159 160 protected fun hideLoadingView() { 161 loadingView?.let { 162 val viewGroup = it.parent as ViewGroup? 163 viewGroup?.removeView(it) 164 it.hide() 165 } 166 loadingView = null 167 } 168 169 protected fun removeAllViewsFromContainer() { 170 containerView.removeAllViews() 171 } 172 // region Loading 173 /** 174 * Successfully finished loading 175 */ 176 @UiThread 177 protected fun finishLoading() { 178 waitForReactAndFinishLoading() 179 } 180 181 /** 182 * There was an error during loading phase 183 */ 184 protected fun interruptLoading() { 185 handler.removeCallbacksAndMessages(null) 186 } 187 188 // Loop until a view is added to the ReactRootView and once it happens run callback 189 private fun waitForReactRootViewToHaveChildrenAndRunCallback(callback: Runnable) { 190 if (reactRootView.isNull) { 191 return 192 } 193 194 if (reactRootView.call("getChildCount") as Int > 0) { 195 callback.run() 196 } else { 197 handler.postDelayed( 198 { waitForReactRootViewToHaveChildrenAndRunCallback(callback) }, 199 VIEW_TEST_INTERVAL_MS 200 ) 201 } 202 } 203 204 /** 205 * Waits for JS side of React to be launched and then performs final launching actions. 206 */ 207 private fun waitForReactAndFinishLoading() { 208 if (Constants.isStandaloneApp() && Constants.SHOW_LOADING_VIEW_IN_SHELL_APP) { 209 val layoutParams = containerView.layoutParams 210 layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT 211 containerView.layoutParams = layoutParams 212 } 213 214 try { 215 // NOTE(evanbacon): Use the same view as the `expo-system-ui` module. 216 // Set before the application code runs to ensure immediate SystemUI calls overwrite the app.json value. 217 var rootView = this.window.decorView 218 ExperienceActivityUtils.setRootViewBackgroundColor(manifest!!, rootView) 219 } catch (e: Exception) { 220 EXL.e(TAG, e) 221 } 222 223 waitForReactRootViewToHaveChildrenAndRunCallback { 224 onDoneLoading() 225 try { 226 // NOTE(evanbacon): The hierarchy at this point looks like: 227 // window.decorView > [4 other views] > containerView > reactContainerView > rootView > [RN App] 228 // This can be inspected using Android Studio: View > Tool Windows > Layout Inspector. 229 // Container background color is set for "loading" view state, we need to set it to transparent to prevent obstructing the root view. 230 containerView!!.setBackgroundColor(Color.TRANSPARENT) 231 } catch (e: Exception) { 232 EXL.e(TAG, e) 233 } 234 ErrorRecoveryManager.getInstance(experienceKey!!).markExperienceLoaded() 235 pollForEventsToSendToRN() 236 EventBus.getDefault().post(ExperienceDoneLoadingEvent(this)) 237 isLoading = false 238 } 239 } 240 // endregion 241 // region SplashScreen 242 /** 243 * Get what version (among versioned classes) of ReactRootView.class SplashScreen module should be looking for. 244 */ 245 protected fun getRootViewClass(manifest: Manifest): Class<out ViewGroup> { 246 val reactRootViewRNClass = reactRootView.rnClass() 247 if (reactRootViewRNClass != null) { 248 return reactRootViewRNClass as Class<out ViewGroup> 249 } 250 var sdkVersion = manifest.getExpoGoSDKVersion() 251 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == this.sdkVersion) { 252 sdkVersion = RNObject.UNVERSIONED 253 } 254 sdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 255 return RNObject("com.facebook.react.ReactRootView").loadVersion(sdkVersion!!).rnClass() as Class<out ViewGroup> 256 } 257 258 // endregion 259 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 260 devSupportManager?.let { devSupportManager -> 261 if (!isCrashed && devSupportManager.call("getDevSupportEnabled") as Boolean) { 262 val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer) 263 .didDoubleTapR(keyCode, currentFocus) 264 if (didDoubleTapR) { 265 devSupportManager.call("reloadExpoApp") 266 return true 267 } 268 } 269 } 270 271 return super.onKeyUp(keyCode, event) 272 } 273 274 override fun onBackPressed() { 275 if (reactInstanceManager.isNotNull && !isCrashed) { 276 reactInstanceManager.call("onBackPressed") 277 } else { 278 super.onBackPressed() 279 } 280 } 281 282 override fun invokeDefaultOnBackPressed() { 283 super.onBackPressed() 284 } 285 286 override fun onPause() { 287 super.onPause() 288 if (reactInstanceManager.isNotNull && !isCrashed) { 289 KernelNetworkInterceptor.onPause() 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 KernelNetworkInterceptor.onResume(reactInstanceManager.get()) 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 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 if (manifest?.jsEngine == "hermes") { 475 // Disable remote debugging when running on Hermes 476 devSettings.call("setRemoteJSDebugEnabled", false) 477 } 478 waitForReactAndFinishLoading() 479 } 480 } 481 482 mReactInstanceManager.onHostResume(this, this) 483 val appKey = manifest!!.getAppKey() 484 reactRootView.call( 485 "startReactApplication", 486 mReactInstanceManager.get(), 487 appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY, 488 initialProps(bundle) 489 ) 490 491 KernelNetworkInterceptor.start(manifest!!, mReactInstanceManager.get()) 492 493 // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager} 494 // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}. 495 // Originally react-native will automatically attach after `startReactApplication`. 496 // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8, 497 // the only remaining path is by `onMeasure`. 498 reactRootView.call("requestLayout") 499 500 return mReactInstanceManager 501 } 502 503 protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean { 504 if (isLoading) { 505 // Don't hit ErrorRecoveryManager until bridge is initialized. 506 // This is the same on iOS. 507 return true 508 } 509 510 val errorRecoveryManager = experienceKey?.let { ErrorRecoveryManager.getInstance(it) } 511 errorRecoveryManager?.markErrored() 512 if (errorRecoveryManager?.shouldReloadOnError() != true) { 513 return true 514 } 515 516 manifestUrl?.let { 517 // Kernel couldn't reload, show error screen 518 if (!KernelProvider.instance.reloadVisibleExperience(it)) { 519 return true 520 } 521 } 522 523 errorQueue.clear() 524 525 return false 526 } 527 528 fun onEventMainThread(event: AddedExperienceEventEvent) { 529 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 530 pollForEventsToSendToRN() 531 } 532 } 533 534 fun onEvent(event: ExperienceContentLoaded?) {} 535 536 private fun pollForEventsToSendToRN() { 537 if (manifestUrl == null) { 538 return 539 } 540 541 try { 542 val rctDeviceEventEmitter = 543 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 544 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 545 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 546 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 547 if (existingEmitter != null) { 548 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 549 for ((eventName, eventPayload) in events) { 550 existingEmitter.call("emit", eventName, eventPayload) 551 } 552 } 553 } catch (e: Throwable) { 554 EXL.e(TAG, e) 555 } 556 } 557 558 /** 559 * Emits events to `RCTNativeAppEventEmitter` 560 */ 561 fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) { 562 try { 563 val nativeAppEventEmitter = 564 RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter") 565 nativeAppEventEmitter.loadVersion(detachSdkVersion!!) 566 val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 567 .callRecursive("getJSModule", nativeAppEventEmitter.rnClass()) 568 emitter?.call("emit", eventName, eventArgs) 569 } catch (e: Throwable) { 570 EXL.e(TAG, e) 571 } 572 } 573 574 // for getting global permission 575 override fun checkSelfPermission(permission: String): Int { 576 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 577 } 578 579 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 580 // in scoped application we don't have `don't ask again` button 581 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 582 true 583 } else super.shouldShowRequestPermissionRationale(permission) 584 } 585 586 override fun requestPermissions( 587 permissions: Array<String>, 588 requestCode: Int, 589 listener: PermissionListener 590 ) { 591 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 592 val name = manifest!!.getName() 593 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!) 594 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 595 } else { 596 super.requestPermissions(permissions, requestCode) 597 } 598 } 599 600 override fun onRequestPermissionsResult( 601 requestCode: Int, 602 permissions: Array<String>, 603 grantResults: IntArray 604 ) { 605 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 606 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 607 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 608 scopedPermissionsRequester = null 609 } 610 } 611 } else { 612 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 613 } 614 } 615 616 // for getting scoped permission 617 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 618 val globalResult = super.checkPermission(permission, pid, uid) 619 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 620 globalResult, 621 packageManager, 622 permission, 623 experienceKey!! 624 ) 625 } 626 627 val devSupportManager: RNObject? 628 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager") 629 630 val jsExecutorName: String? 631 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String 632 633 // deprecated in favor of Expo.Linking.makeUrl 634 // TODO: remove this 635 private val linkingUri: String? 636 get() = if (Constants.SHELL_APP_SCHEME != null) { 637 Constants.SHELL_APP_SCHEME + "://" 638 } else { 639 val uri = Uri.parse(manifestUrl) 640 val host = uri.host 641 if (host != null && ( 642 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 643 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 644 ".expo.test" 645 ) 646 ) 647 ) { 648 val pathSegments = uri.pathSegments 649 val builder = uri.buildUpon() 650 builder.path(null) 651 for (segment in pathSegments) { 652 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 653 break 654 } 655 builder.appendEncodedPath(segment) 656 } 657 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 658 .toString() 659 } else { 660 manifestUrl 661 } 662 } 663 664 companion object { 665 private val TAG = ReactNativeActivity::class.java.simpleName 666 private const val VIEW_TEST_INTERVAL_MS: Long = 20 667 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 668 } 669 } 670