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 manifestUrl?.let { 518 // Kernel couldn't reload, show error screen 519 if (!KernelProvider.instance.reloadVisibleExperience(it)) { 520 return true 521 } 522 } 523 524 errorQueue.clear() 525 526 return false 527 } 528 529 fun onEventMainThread(event: AddedExperienceEventEvent) { 530 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 531 pollForEventsToSendToRN() 532 } 533 } 534 535 fun onEvent(event: ExperienceContentLoaded?) {} 536 537 private fun pollForEventsToSendToRN() { 538 if (manifestUrl == null) { 539 return 540 } 541 542 try { 543 val rctDeviceEventEmitter = 544 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 545 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 546 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 547 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 548 if (existingEmitter != null) { 549 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 550 for ((eventName, eventPayload) in events) { 551 existingEmitter.call("emit", eventName, eventPayload) 552 } 553 } 554 } catch (e: Throwable) { 555 EXL.e(TAG, e) 556 } 557 } 558 559 /** 560 * Emits events to `RCTNativeAppEventEmitter` 561 */ 562 fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) { 563 try { 564 val nativeAppEventEmitter = 565 RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter") 566 nativeAppEventEmitter.loadVersion(detachSdkVersion!!) 567 val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 568 .callRecursive("getJSModule", nativeAppEventEmitter.rnClass()) 569 emitter?.call("emit", eventName, eventArgs) 570 } catch (e: Throwable) { 571 EXL.e(TAG, e) 572 } 573 } 574 575 // for getting global permission 576 override fun checkSelfPermission(permission: String): Int { 577 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 578 } 579 580 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 581 // in scoped application we don't have `don't ask again` button 582 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 583 true 584 } else super.shouldShowRequestPermissionRationale(permission) 585 } 586 587 override fun requestPermissions( 588 permissions: Array<String>, 589 requestCode: Int, 590 listener: PermissionListener 591 ) { 592 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 593 val name = manifest!!.getName() 594 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!) 595 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 596 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 597 super.requestPermissions(permissions, requestCode) 598 } 599 } 600 601 override fun onRequestPermissionsResult( 602 requestCode: Int, 603 permissions: Array<String>, 604 grantResults: IntArray 605 ) { 606 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 607 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 608 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 609 scopedPermissionsRequester = null 610 } 611 } 612 } else { 613 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 614 } 615 } 616 617 // for getting scoped permission 618 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 619 val globalResult = super.checkPermission(permission, pid, uid) 620 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 621 globalResult, 622 packageManager, 623 permission, 624 experienceKey!! 625 ) 626 } 627 628 val devSupportManager: RNObject? 629 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager") 630 631 val jsExecutorName: String? 632 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String 633 634 // deprecated in favor of Expo.Linking.makeUrl 635 // TODO: remove this 636 private val linkingUri: String? 637 get() = if (Constants.SHELL_APP_SCHEME != null) { 638 Constants.SHELL_APP_SCHEME + "://" 639 } else { 640 val uri = Uri.parse(manifestUrl) 641 val host = uri.host 642 if (host != null && ( 643 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 644 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 645 ".expo.test" 646 ) 647 ) 648 ) { 649 val pathSegments = uri.pathSegments 650 val builder = uri.buildUpon() 651 builder.path(null) 652 for (segment in pathSegments) { 653 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 654 break 655 } 656 builder.appendEncodedPath(segment) 657 } 658 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 659 .toString() 660 } else { 661 manifestUrl 662 } 663 } 664 665 companion object { 666 private val TAG = ReactNativeActivity::class.java.simpleName 667 private const val VIEW_TEST_INTERVAL_MS: Long = 20 668 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 669 } 670 } 671