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 KernelNetworkInterceptor.onPause() 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 KernelNetworkInterceptor.onResume(reactInstanceManager.get()) 301 } 302 } 303 304 override fun onDestroy() { 305 super.onDestroy() 306 destroyReactInstanceManager() 307 handler.removeCallbacksAndMessages(null) 308 EventBus.getDefault().unregister(this) 309 } 310 311 public override fun onNewIntent(intent: Intent) { 312 if (reactInstanceManager.isNotNull && !isCrashed) { 313 try { 314 reactInstanceManager.call("onNewIntent", intent) 315 } catch (e: Throwable) { 316 EXL.e(TAG, e.toString()) 317 super.onNewIntent(intent) 318 } 319 } else { 320 super.onNewIntent(intent) 321 } 322 } 323 324 open val isDebugModeEnabled: Boolean 325 get() = manifest?.isDevelopmentMode() ?: false 326 327 protected open fun destroyReactInstanceManager() { 328 if (reactInstanceManager.isNotNull && !isCrashed) { 329 reactInstanceManager.call("destroy") 330 } 331 } 332 333 public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 334 super.onActivityResult(requestCode, resultCode, data) 335 336 Exponent.instance.onActivityResult(requestCode, resultCode, data) 337 338 if (reactInstanceManager.isNotNull && !isCrashed) { 339 reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data) 340 } 341 342 // Have permission to draw over other apps. Resume loading. 343 if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) { 344 // startReactInstance() checks isInForeground and onActivityResult is called before onResume, 345 // so manually set this here. 346 isInForeground = true 347 startReactInstance() 348 } 349 } 350 351 fun startReactInstance( 352 delegate: StartReactInstanceDelegate, 353 intentUri: String?, 354 sdkVersion: String?, 355 notification: ExponentNotification?, 356 isShellApp: Boolean, 357 extraNativeModules: List<Any>?, 358 extraExpoPackages: List<Package>?, 359 progressListener: DevBundleDownloadProgressListener 360 ): RNObject { 361 if (isCrashed || !delegate.isInForeground) { 362 // Can sometimes get here after an error has occurred. Return early or else we'll hit 363 // a null pointer at mReactRootView.startReactApplication 364 return RNObject("com.facebook.react.ReactInstanceManager") 365 } 366 367 val experienceProperties = mapOf<String, Any?>( 368 KernelConstants.MANIFEST_URL_KEY to manifestUrl, 369 KernelConstants.LINKING_URI_KEY to linkingUri, 370 KernelConstants.INTENT_URI_KEY to intentUri, 371 KernelConstants.IS_HEADLESS_KEY to false 372 ) 373 374 val instanceManagerBuilderProperties = InstanceManagerBuilderProperties( 375 application = application, 376 jsBundlePath = jsBundlePath, 377 experienceProperties = experienceProperties, 378 expoPackages = extraExpoPackages, 379 exponentPackageDelegate = delegate.exponentPackageDelegate, 380 manifest = manifest!!, 381 singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages) 382 ) 383 384 val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!) 385 val builder = versionedUtils.callRecursive( 386 "getReactInstanceManagerBuilder", 387 instanceManagerBuilderProperties 388 )!! 389 390 builder.call("setCurrentActivity", this) 391 392 // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here 393 builder.call( 394 "setInitialLifecycleState", 395 RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED") 396 ) 397 398 if (extraNativeModules != null) { 399 for (nativeModule in extraNativeModules) { 400 builder.call("addPackage", nativeModule) 401 } 402 } 403 404 if (delegate.isDebugModeEnabled) { 405 val debuggerHost = manifest!!.getDebuggerHost() 406 val mainModuleName = manifest!!.getMainModuleName() 407 Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder) 408 409 val devLoadingView = 410 RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion) 411 devLoadingView.callRecursive("setDevLoadingEnabled", false) 412 413 val devBundleDownloadListener = 414 RNObject("host.exp.exponent.ExponentDevBundleDownloadListener") 415 .loadVersion(sdkVersion) 416 .construct(progressListener) 417 builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get()) 418 } else { 419 waitForReactAndFinishLoading() 420 } 421 422 val bundle = Bundle() 423 val exponentProps = JSONObject() 424 if (notification != null) { 425 bundle.putString("notification", notification.body) // Deprecated 426 try { 427 exponentProps.put("notification", notification.toJSONObject("selected")) 428 } catch (e: JSONException) { 429 e.printStackTrace() 430 } 431 } 432 433 try { 434 exponentProps.put("manifestString", manifest.toString()) 435 exponentProps.put("shell", isShellApp) 436 exponentProps.put("initialUri", intentUri) 437 } catch (e: JSONException) { 438 EXL.e(TAG, e) 439 } 440 441 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!) 442 if (metadata != null) { 443 // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience, 444 // we need to send them with the standard notification events so that you can get all the unread notification through an event 445 // Copy unreadNotifications into exponentProps 446 if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) { 447 try { 448 val unreadNotifications = 449 metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 450 delegate.handleUnreadNotifications(unreadNotifications) 451 } catch (e: JSONException) { 452 e.printStackTrace() 453 } 454 metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 455 } 456 exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata) 457 } 458 459 try { 460 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps)) 461 } catch (e: JSONException) { 462 throw Error("JSONObject failed to be converted to Bundle", e) 463 } 464 465 if (!delegate.isInForeground) { 466 return RNObject("com.facebook.react.ReactInstanceManager") 467 } 468 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 if (manifest?.jsEngine == "hermes") { 476 // Disable remote debugging when running on Hermes 477 devSettings.call("setRemoteJSDebugEnabled", false) 478 } 479 waitForReactAndFinishLoading() 480 } 481 } 482 483 mReactInstanceManager.onHostResume(this, this) 484 val appKey = manifest!!.getAppKey() 485 reactRootView.call( 486 "startReactApplication", 487 mReactInstanceManager.get(), 488 appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY, 489 initialProps(bundle) 490 ) 491 492 KernelNetworkInterceptor.start(manifest!!, mReactInstanceManager.get()) 493 494 // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager} 495 // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}. 496 // Originally react-native will automatically attach after `startReactApplication`. 497 // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8, 498 // the only remaining path is by `onMeasure`. 499 reactRootView.call("requestLayout") 500 501 return mReactInstanceManager 502 } 503 504 protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean { 505 if (isLoading) { 506 // Don't hit ErrorRecoveryManager until bridge is initialized. 507 // This is the same on iOS. 508 return true 509 } 510 val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!) 511 errorRecoveryManager.markErrored() 512 513 if (!errorRecoveryManager.shouldReloadOnError()) { 514 return true 515 } 516 517 if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) { 518 // Kernel couldn't reload, show error screen 519 return true 520 } 521 522 errorQueue.clear() 523 524 return false 525 } 526 527 fun onEventMainThread(event: AddedExperienceEventEvent) { 528 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 529 pollForEventsToSendToRN() 530 } 531 } 532 533 fun onEvent(event: ExperienceContentLoaded?) {} 534 535 private fun pollForEventsToSendToRN() { 536 if (manifestUrl == null) { 537 return 538 } 539 540 try { 541 val rctDeviceEventEmitter = 542 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 543 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 544 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 545 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 546 if (existingEmitter != null) { 547 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 548 for ((eventName, eventPayload) in events) { 549 existingEmitter.call("emit", eventName, eventPayload) 550 } 551 } 552 } catch (e: Throwable) { 553 EXL.e(TAG, e) 554 } 555 } 556 557 /** 558 * Emits events to `RCTNativeAppEventEmitter` 559 */ 560 fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) { 561 try { 562 val nativeAppEventEmitter = 563 RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter") 564 nativeAppEventEmitter.loadVersion(detachSdkVersion!!) 565 val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 566 .callRecursive("getJSModule", nativeAppEventEmitter.rnClass()) 567 emitter?.call("emit", eventName, eventArgs) 568 } catch (e: Throwable) { 569 EXL.e(TAG, e) 570 } 571 } 572 573 // for getting global permission 574 override fun checkSelfPermission(permission: String): Int { 575 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 576 } 577 578 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 579 // in scoped application we don't have `don't ask again` button 580 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 581 true 582 } else super.shouldShowRequestPermissionRationale(permission) 583 } 584 585 override fun requestPermissions( 586 permissions: Array<String>, 587 requestCode: Int, 588 listener: PermissionListener 589 ) { 590 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 591 val name = manifest!!.getName() 592 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!) 593 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 594 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 595 super.requestPermissions(permissions, requestCode) 596 } 597 } 598 599 override fun onRequestPermissionsResult( 600 requestCode: Int, 601 permissions: Array<String>, 602 grantResults: IntArray 603 ) { 604 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 605 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 606 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 607 scopedPermissionsRequester = null 608 } 609 } 610 } else { 611 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 612 } 613 } 614 615 // for getting scoped permission 616 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 617 val globalResult = super.checkPermission(permission, pid, uid) 618 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 619 globalResult, 620 packageManager, 621 permission, 622 experienceKey!! 623 ) 624 } 625 626 val devSupportManager: RNObject? 627 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager") 628 629 val jsExecutorName: String? 630 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String 631 632 // deprecated in favor of Expo.Linking.makeUrl 633 // TODO: remove this 634 private val linkingUri: String? 635 get() = if (Constants.SHELL_APP_SCHEME != null) { 636 Constants.SHELL_APP_SCHEME + "://" 637 } else { 638 val uri = Uri.parse(manifestUrl) 639 val host = uri.host 640 if (host != null && ( 641 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 642 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 643 ".expo.test" 644 ) 645 ) 646 ) { 647 val pathSegments = uri.pathSegments 648 val builder = uri.buildUpon() 649 builder.path(null) 650 for (segment in pathSegments) { 651 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 652 break 653 } 654 builder.appendEncodedPath(segment) 655 } 656 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 657 .toString() 658 } else { 659 manifestUrl 660 } 661 } 662 663 companion object { 664 private val TAG = ReactNativeActivity::class.java.simpleName 665 private const val VIEW_TEST_INTERVAL_MS: Long = 20 666 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 667 } 668 } 669