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