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 val networkInterceptor = 75 RNObject("host.exp.exponent.ExpoNetworkInterceptor") 76 77 protected var manifestUrl: String? = null 78 var experienceKey: ExperienceKey? = null 79 protected var sdkVersion: String? = null 80 protected var activityId = 0 81 82 // In detach we want UNVERSIONED most places. We still need the numbered sdk version 83 // when creating cache keys. 84 protected var detachSdkVersion: String? = null 85 86 protected lateinit var reactRootView: RNObject 87 private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer 88 var isLoading = true 89 protected set 90 protected var jsBundlePath: String? = null 91 protected var manifest: Manifest? = null 92 var isInForeground = false 93 protected set 94 private var scopedPermissionsRequester: ScopedPermissionsRequester? = null 95 96 @Inject 97 protected lateinit var exponentSharedPreferences: ExponentSharedPreferences 98 99 @Inject 100 lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry 101 102 private lateinit var containerView: FrameLayout 103 104 /** 105 * This view is optional and available only when the app runs in Expo Go. 106 */ 107 private var loadingView: LoadingView? = null 108 private lateinit var reactContainerView: FrameLayout 109 private val handler = Handler() 110 111 protected open fun shouldCreateLoadingView(): Boolean { 112 return !Constants.isStandaloneApp() || Constants.SHOW_LOADING_VIEW_IN_SHELL_APP 113 } 114 115 val rootView: View? 116 get() = reactRootView.get() as View? 117 118 override fun onCreate(savedInstanceState: Bundle?) { 119 super.onCreate(null) 120 121 containerView = FrameLayout(this) 122 setContentView(containerView) 123 124 reactContainerView = FrameLayout(this) 125 containerView.addView(reactContainerView) 126 127 if (shouldCreateLoadingView()) { 128 containerView.setBackgroundColor( 129 ContextCompat.getColor( 130 this, 131 R.color.splashscreen_background 132 ) 133 ) 134 loadingView = LoadingView(this) 135 loadingView!!.show() 136 containerView.addView(loadingView) 137 } 138 139 doubleTapReloadRecognizer = DoubleTapReloadRecognizer() 140 Exponent.initialize(this, application) 141 NativeModuleDepsProvider.instance.inject(ReactNativeActivity::class.java, this) 142 143 // Can't call this here because subclasses need to do other initialization 144 // before their listener methods are called. 145 // EventBus.getDefault().registerSticky(this); 146 } 147 148 protected fun setReactRootView(reactRootView: View) { 149 reactContainerView.removeAllViews() 150 addReactViewToContentContainer(reactRootView) 151 } 152 153 fun addReactViewToContentContainer(reactView: View) { 154 if (reactView.parent != null) { 155 (reactView.parent as ViewGroup).removeView(reactView) 156 } 157 reactContainerView.addView(reactView) 158 } 159 160 fun hasReactView(reactView: View): Boolean { 161 return reactView.parent === reactContainerView 162 } 163 164 protected fun hideLoadingView() { 165 loadingView?.let { 166 val viewGroup = it.parent as ViewGroup? 167 viewGroup?.removeView(it) 168 it.hide() 169 } 170 loadingView = null 171 } 172 173 protected fun removeAllViewsFromContainer() { 174 containerView.removeAllViews() 175 } 176 // region Loading 177 /** 178 * Successfully finished loading 179 */ 180 @UiThread 181 protected fun finishLoading() { 182 waitForReactAndFinishLoading() 183 } 184 185 /** 186 * There was an error during loading phase 187 */ 188 protected fun interruptLoading() { 189 handler.removeCallbacksAndMessages(null) 190 } 191 192 // Loop until a view is added to the ReactRootView and once it happens run callback 193 private fun waitForReactRootViewToHaveChildrenAndRunCallback(callback: Runnable) { 194 if (reactRootView.isNull) { 195 return 196 } 197 198 if (reactRootView.call("getChildCount") as Int > 0) { 199 callback.run() 200 } else { 201 handler.postDelayed( 202 { waitForReactRootViewToHaveChildrenAndRunCallback(callback) }, 203 VIEW_TEST_INTERVAL_MS 204 ) 205 } 206 } 207 208 /** 209 * Waits for JS side of React to be launched and then performs final launching actions. 210 */ 211 private fun waitForReactAndFinishLoading() { 212 if (Constants.isStandaloneApp() && Constants.SHOW_LOADING_VIEW_IN_SHELL_APP) { 213 val layoutParams = containerView.layoutParams 214 layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT 215 containerView.layoutParams = layoutParams 216 } 217 218 try { 219 // NOTE(evanbacon): Use the same view as the `expo-system-ui` module. 220 // Set before the application code runs to ensure immediate SystemUI calls overwrite the app.json value. 221 var rootView = this.window.decorView 222 ExperienceActivityUtils.setRootViewBackgroundColor(manifest!!, rootView) 223 } catch (e: Exception) { 224 EXL.e(TAG, e) 225 } 226 227 waitForReactRootViewToHaveChildrenAndRunCallback { 228 onDoneLoading() 229 try { 230 // NOTE(evanbacon): The hierarchy at this point looks like: 231 // window.decorView > [4 other views] > containerView > reactContainerView > rootView > [RN App] 232 // This can be inspected using Android Studio: View > Tool Windows > Layout Inspector. 233 // Container background color is set for "loading" view state, we need to set it to transparent to prevent obstructing the root view. 234 containerView!!.setBackgroundColor(Color.TRANSPARENT) 235 } catch (e: Exception) { 236 EXL.e(TAG, e) 237 } 238 ErrorRecoveryManager.getInstance(experienceKey!!).markExperienceLoaded() 239 pollForEventsToSendToRN() 240 EventBus.getDefault().post(ExperienceDoneLoadingEvent(this)) 241 isLoading = false 242 } 243 } 244 // endregion 245 // region SplashScreen 246 /** 247 * Get what version (among versioned classes) of ReactRootView.class SplashScreen module should be looking for. 248 */ 249 protected fun getRootViewClass(manifest: Manifest): Class<out ViewGroup> { 250 val reactRootViewRNClass = reactRootView.rnClass() 251 if (reactRootViewRNClass != null) { 252 return reactRootViewRNClass as Class<out ViewGroup> 253 } 254 var sdkVersion = manifest.getExpoGoSDKVersion() 255 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == this.sdkVersion) { 256 sdkVersion = RNObject.UNVERSIONED 257 } 258 sdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 259 return RNObject("com.facebook.react.ReactRootView").loadVersion(sdkVersion!!).rnClass() as Class<out ViewGroup> 260 } 261 262 // endregion 263 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 264 devSupportManager?.let { devSupportManager -> 265 if (!isCrashed && devSupportManager.call("getDevSupportEnabled") as Boolean) { 266 val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer) 267 .didDoubleTapR(keyCode, currentFocus) 268 if (didDoubleTapR) { 269 devSupportManager.call("reloadExpoApp") 270 return true 271 } 272 } 273 } 274 275 return super.onKeyUp(keyCode, event) 276 } 277 278 override fun onBackPressed() { 279 if (reactInstanceManager.isNotNull && !isCrashed) { 280 reactInstanceManager.call("onBackPressed") 281 } else { 282 super.onBackPressed() 283 } 284 } 285 286 override fun invokeDefaultOnBackPressed() { 287 super.onBackPressed() 288 } 289 290 override fun onPause() { 291 super.onPause() 292 if (reactInstanceManager.isNotNull && !isCrashed) { 293 networkInterceptor.call("onPause") 294 reactInstanceManager.onHostPause() 295 // TODO: use onHostPause(activity) 296 } 297 } 298 299 override fun onResume() { 300 super.onResume() 301 if (reactInstanceManager.isNotNull && !isCrashed) { 302 reactInstanceManager.onHostResume(this, this) 303 networkInterceptor.call("onResume", reactInstanceManager.get()) 304 } 305 } 306 307 override fun onDestroy() { 308 super.onDestroy() 309 destroyReactInstanceManager() 310 handler.removeCallbacksAndMessages(null) 311 EventBus.getDefault().unregister(this) 312 } 313 314 public override fun onNewIntent(intent: Intent) { 315 if (reactInstanceManager.isNotNull && !isCrashed) { 316 try { 317 reactInstanceManager.call("onNewIntent", intent) 318 } catch (e: Throwable) { 319 EXL.e(TAG, e.toString()) 320 super.onNewIntent(intent) 321 } 322 } else { 323 super.onNewIntent(intent) 324 } 325 } 326 327 open val isDebugModeEnabled: Boolean 328 get() = manifest?.isDevelopmentMode() ?: false 329 330 protected open fun destroyReactInstanceManager() { 331 if (reactInstanceManager.isNotNull && !isCrashed) { 332 reactInstanceManager.call("destroy") 333 } 334 } 335 336 public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 337 super.onActivityResult(requestCode, resultCode, data) 338 339 Exponent.instance.onActivityResult(requestCode, resultCode, data) 340 341 if (reactInstanceManager.isNotNull && !isCrashed) { 342 reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data) 343 } 344 345 // Have permission to draw over other apps. Resume loading. 346 if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) { 347 // startReactInstance() checks isInForeground and onActivityResult is called before onResume, 348 // so manually set this here. 349 isInForeground = true 350 startReactInstance() 351 } 352 } 353 354 fun startReactInstance( 355 delegate: StartReactInstanceDelegate, 356 intentUri: String?, 357 sdkVersion: String?, 358 notification: ExponentNotification?, 359 isShellApp: Boolean, 360 extraNativeModules: List<Any>?, 361 extraExpoPackages: List<Package>?, 362 progressListener: DevBundleDownloadProgressListener 363 ): RNObject { 364 if (isCrashed || !delegate.isInForeground) { 365 // Can sometimes get here after an error has occurred. Return early or else we'll hit 366 // a null pointer at mReactRootView.startReactApplication 367 return RNObject("com.facebook.react.ReactInstanceManager") 368 } 369 370 val experienceProperties = mapOf<String, Any?>( 371 KernelConstants.MANIFEST_URL_KEY to manifestUrl, 372 KernelConstants.LINKING_URI_KEY to linkingUri, 373 KernelConstants.INTENT_URI_KEY to intentUri, 374 KernelConstants.IS_HEADLESS_KEY to false 375 ) 376 377 val instanceManagerBuilderProperties = InstanceManagerBuilderProperties( 378 application = application, 379 jsBundlePath = jsBundlePath, 380 experienceProperties = experienceProperties, 381 expoPackages = extraExpoPackages, 382 exponentPackageDelegate = delegate.exponentPackageDelegate, 383 manifest = manifest!!, 384 singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages) 385 ) 386 387 val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!) 388 val builder = versionedUtils.callRecursive( 389 "getReactInstanceManagerBuilder", 390 instanceManagerBuilderProperties 391 )!! 392 393 builder.call("setCurrentActivity", this) 394 395 // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here 396 builder.call( 397 "setInitialLifecycleState", 398 RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED") 399 ) 400 401 if (extraNativeModules != null) { 402 for (nativeModule in extraNativeModules) { 403 builder.call("addPackage", nativeModule) 404 } 405 } 406 407 if (delegate.isDebugModeEnabled) { 408 val debuggerHost = manifest!!.getDebuggerHost() 409 val mainModuleName = manifest!!.getMainModuleName() 410 Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder) 411 412 val devLoadingView = 413 RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion) 414 devLoadingView.callRecursive("setDevLoadingEnabled", false) 415 416 val devBundleDownloadListener = 417 RNObject("host.exp.exponent.ExponentDevBundleDownloadListener") 418 .loadVersion(sdkVersion) 419 .construct(progressListener) 420 builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get()) 421 } else { 422 waitForReactAndFinishLoading() 423 } 424 425 val bundle = Bundle() 426 val exponentProps = JSONObject() 427 if (notification != null) { 428 bundle.putString("notification", notification.body) // Deprecated 429 try { 430 exponentProps.put("notification", notification.toJSONObject("selected")) 431 } catch (e: JSONException) { 432 e.printStackTrace() 433 } 434 } 435 436 try { 437 exponentProps.put("manifestString", manifest.toString()) 438 exponentProps.put("shell", isShellApp) 439 exponentProps.put("initialUri", intentUri) 440 } catch (e: JSONException) { 441 EXL.e(TAG, e) 442 } 443 444 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!) 445 if (metadata != null) { 446 // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience, 447 // we need to send them with the standard notification events so that you can get all the unread notification through an event 448 // Copy unreadNotifications into exponentProps 449 if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) { 450 try { 451 val unreadNotifications = 452 metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 453 delegate.handleUnreadNotifications(unreadNotifications) 454 } catch (e: JSONException) { 455 e.printStackTrace() 456 } 457 metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 458 } 459 exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata) 460 } 461 462 try { 463 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps)) 464 } catch (e: JSONException) { 465 throw Error("JSONObject failed to be converted to Bundle", e) 466 } 467 468 if (!delegate.isInForeground) { 469 return RNObject("com.facebook.react.ReactInstanceManager") 470 } 471 472 val mReactInstanceManager = builder.callRecursive("build")!! 473 val devSettings = 474 mReactInstanceManager.callRecursive("getDevSupportManager")!!.callRecursive("getDevSettings") 475 if (devSettings != null) { 476 devSettings.setField("exponentActivityId", activityId) 477 if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) { 478 if (manifest?.jsEngine == "hermes") { 479 // Disable remote debugging when running on Hermes 480 devSettings.call("setRemoteJSDebugEnabled", false) 481 } 482 waitForReactAndFinishLoading() 483 } 484 } 485 486 mReactInstanceManager.onHostResume(this, this) 487 val appKey = manifest!!.getAppKey() 488 reactRootView.call( 489 "startReactApplication", 490 mReactInstanceManager.get(), 491 appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY, 492 initialProps(bundle) 493 ) 494 495 networkInterceptor.loadVersion(sdkVersion).construct().call("start", manifest, mReactInstanceManager.get()) 496 497 // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager} 498 // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}. 499 // Originally react-native will automatically attach after `startReactApplication`. 500 // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8, 501 // the only remaining path is by `onMeasure`. 502 reactRootView.call("requestLayout") 503 504 return mReactInstanceManager 505 } 506 507 protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean { 508 if (isLoading) { 509 // Don't hit ErrorRecoveryManager until bridge is initialized. 510 // This is the same on iOS. 511 return true 512 } 513 val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!) 514 errorRecoveryManager.markErrored() 515 516 if (!errorRecoveryManager.shouldReloadOnError()) { 517 return true 518 } 519 520 if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) { 521 // Kernel couldn't reload, show error screen 522 return true 523 } 524 525 errorQueue.clear() 526 527 return false 528 } 529 530 fun onEventMainThread(event: AddedExperienceEventEvent) { 531 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 532 pollForEventsToSendToRN() 533 } 534 } 535 536 fun onEvent(event: ExperienceContentLoaded?) {} 537 538 private fun pollForEventsToSendToRN() { 539 if (manifestUrl == null) { 540 return 541 } 542 543 try { 544 val rctDeviceEventEmitter = 545 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 546 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 547 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 548 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 549 if (existingEmitter != null) { 550 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 551 for ((eventName, eventPayload) in events) { 552 existingEmitter.call("emit", eventName, eventPayload) 553 } 554 } 555 } catch (e: Throwable) { 556 EXL.e(TAG, e) 557 } 558 } 559 560 /** 561 * Emits events to `RCTNativeAppEventEmitter` 562 */ 563 fun emitRCTNativeAppEvent(eventName: String, eventArgs: Map<String, String>?) { 564 try { 565 val nativeAppEventEmitter = 566 RNObject("com.facebook.react.modules.core.RCTNativeAppEventEmitter") 567 nativeAppEventEmitter.loadVersion(detachSdkVersion!!) 568 val emitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 569 .callRecursive("getJSModule", nativeAppEventEmitter.rnClass()) 570 emitter?.call("emit", eventName, eventArgs) 571 } catch (e: Throwable) { 572 EXL.e(TAG, e) 573 } 574 } 575 576 // for getting global permission 577 override fun checkSelfPermission(permission: String): Int { 578 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 579 } 580 581 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 582 // in scoped application we don't have `don't ask again` button 583 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 584 true 585 } else super.shouldShowRequestPermissionRationale(permission) 586 } 587 588 override fun requestPermissions( 589 permissions: Array<String>, 590 requestCode: Int, 591 listener: PermissionListener 592 ) { 593 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 594 val name = manifest!!.getName() 595 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!) 596 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 597 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 598 super.requestPermissions(permissions, requestCode) 599 } 600 } 601 602 override fun onRequestPermissionsResult( 603 requestCode: Int, 604 permissions: Array<String>, 605 grantResults: IntArray 606 ) { 607 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 608 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 609 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 610 scopedPermissionsRequester = null 611 } 612 } 613 } else { 614 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 615 } 616 } 617 618 // for getting scoped permission 619 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 620 val globalResult = super.checkPermission(permission, pid, uid) 621 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 622 globalResult, 623 packageManager, 624 permission, 625 experienceKey!! 626 ) 627 } 628 629 val devSupportManager: RNObject? 630 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getDevSupportManager") 631 632 val jsExecutorName: String? 633 get() = reactInstanceManager.takeIf { it.isNotNull }?.callRecursive("getJsExecutorName")?.get() as? String 634 635 // deprecated in favor of Expo.Linking.makeUrl 636 // TODO: remove this 637 private val linkingUri: String? 638 get() = if (Constants.SHELL_APP_SCHEME != null) { 639 Constants.SHELL_APP_SCHEME + "://" 640 } else { 641 val uri = Uri.parse(manifestUrl) 642 val host = uri.host 643 if (host != null && ( 644 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 645 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 646 ".expo.test" 647 ) 648 ) 649 ) { 650 val pathSegments = uri.pathSegments 651 val builder = uri.buildUpon() 652 builder.path(null) 653 for (segment in pathSegments) { 654 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 655 break 656 } 657 builder.appendEncodedPath(segment) 658 } 659 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 660 .toString() 661 } else { 662 manifestUrl 663 } 664 } 665 666 companion object { 667 private val TAG = ReactNativeActivity::class.java.simpleName 668 private const val VIEW_TEST_INTERVAL_MS: Long = 20 669 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 670 } 671 } 672