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