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.net.Uri 8 import android.os.Build 9 import android.os.Bundle 10 import android.os.Handler 11 import android.os.Process 12 import android.view.KeyEvent 13 import android.view.View 14 import android.view.ViewGroup 15 import android.widget.FrameLayout 16 import androidx.annotation.UiThread 17 import androidx.appcompat.app.AppCompatActivity 18 import androidx.core.content.ContextCompat 19 import com.facebook.infer.annotation.Assertions 20 import com.facebook.internal.BundleJSONConverter 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.updates.manifest.raw.RawManifest 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.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: RawManifest? = 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.getInstance().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 waitForReactRootViewToHaveChildrenAndRunCallback { 215 onDoneLoading() 216 try { 217 ExperienceActivityUtils.setRootViewBackgroundColor(manifest, rootView) 218 } catch (e: Exception) { 219 EXL.e(TAG, e) 220 } 221 ErrorRecoveryManager.getInstance(experienceKey!!).markExperienceLoaded() 222 pollForEventsToSendToRN() 223 EventBus.getDefault().post(ExperienceDoneLoadingEvent(this)) 224 isLoading = false 225 } 226 } 227 // endregion 228 // region SplashScreen 229 /** 230 * Get what version (among versioned classes) of ReactRootView.class SplashScreen module should be looking for. 231 */ 232 protected fun getRootViewClass(manifest: RawManifest): Class<out ViewGroup> { 233 val reactRootViewRNClass = reactRootView.rnClass() 234 if (reactRootViewRNClass != null) { 235 return reactRootViewRNClass as Class<out ViewGroup> 236 } 237 var sdkVersion = manifest.getSDKVersionNullable() 238 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == this.sdkVersion) { 239 sdkVersion = RNObject.UNVERSIONED 240 } 241 sdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 242 return RNObject("com.facebook.react.ReactRootView").loadVersion(sdkVersion).rnClass() as Class<out ViewGroup> 243 } 244 245 // endregion 246 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 247 if (reactInstanceManager.isNotNull && !isCrashed) { 248 if (devSupportManager.call("getDevSupportEnabled") as Boolean) { 249 val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer) 250 .didDoubleTapR(keyCode, currentFocus) 251 if (didDoubleTapR) { 252 devSupportManager.call("reloadExpoApp") 253 return true 254 } 255 } 256 } 257 return super.onKeyUp(keyCode, event) 258 } 259 260 override fun onBackPressed() { 261 if (reactInstanceManager.isNotNull && !isCrashed) { 262 reactInstanceManager.call("onBackPressed") 263 } else { 264 super.onBackPressed() 265 } 266 } 267 268 override fun invokeDefaultOnBackPressed() { 269 super.onBackPressed() 270 } 271 272 override fun onPause() { 273 super.onPause() 274 if (reactInstanceManager.isNotNull && !isCrashed) { 275 reactInstanceManager.onHostPause() 276 // TODO: use onHostPause(activity) 277 } 278 } 279 280 override fun onResume() { 281 super.onResume() 282 if (reactInstanceManager.isNotNull && !isCrashed) { 283 reactInstanceManager.onHostResume(this, this) 284 } 285 } 286 287 override fun onDestroy() { 288 super.onDestroy() 289 destroyReactInstanceManager() 290 handler.removeCallbacksAndMessages(null) 291 EventBus.getDefault().unregister(this) 292 } 293 294 public override fun onNewIntent(intent: Intent) { 295 if (reactInstanceManager.isNotNull && !isCrashed) { 296 try { 297 reactInstanceManager.call("onNewIntent", intent) 298 } catch (e: Throwable) { 299 EXL.e(TAG, e.toString()) 300 super.onNewIntent(intent) 301 } 302 } else { 303 super.onNewIntent(intent) 304 } 305 } 306 307 open val isDebugModeEnabled: Boolean 308 get() = manifest?.isDevelopmentMode() ?: false 309 310 protected open fun destroyReactInstanceManager() { 311 if (reactInstanceManager.isNotNull && !isCrashed) { 312 reactInstanceManager.call("destroy") 313 } 314 } 315 316 public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 317 super.onActivityResult(requestCode, resultCode, data) 318 319 Exponent.instance.onActivityResult(requestCode, resultCode, data) 320 321 if (reactInstanceManager.isNotNull && !isCrashed) { 322 reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data) 323 } 324 325 // Have permission to draw over other apps. Resume loading. 326 if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) { 327 // startReactInstance() checks isInForeground and onActivityResult is called before onResume, 328 // so manually set this here. 329 isInForeground = true 330 startReactInstance() 331 } 332 } 333 334 fun startReactInstance( 335 delegate: StartReactInstanceDelegate, 336 intentUri: String?, 337 sdkVersion: String?, 338 notification: ExponentNotification?, 339 isShellApp: Boolean, 340 extraNativeModules: List<Any>?, 341 extraExpoPackages: List<Package>?, 342 progressListener: DevBundleDownloadProgressListener 343 ): RNObject { 344 if (isCrashed || !delegate.isInForeground) { 345 // Can sometimes get here after an error has occurred. Return early or else we'll hit 346 // a null pointer at mReactRootView.startReactApplication 347 return RNObject("com.facebook.react.ReactInstanceManager") 348 } 349 350 val experienceProperties = mapOf<String, Any?>( 351 KernelConstants.MANIFEST_URL_KEY to manifestUrl, 352 KernelConstants.LINKING_URI_KEY to linkingUri, 353 KernelConstants.INTENT_URI_KEY to intentUri, 354 KernelConstants.IS_HEADLESS_KEY to false 355 ) 356 357 val instanceManagerBuilderProperties = InstanceManagerBuilderProperties().apply { 358 this.application = this@ReactNativeActivity.application 359 this.jsBundlePath = this@ReactNativeActivity.jsBundlePath 360 this.experienceProperties = experienceProperties 361 this.expoPackages = extraExpoPackages 362 this.exponentPackageDelegate = delegate.exponentPackageDelegate 363 this.manifest = this@ReactNativeActivity.manifest 364 this.singletonModules = ExponentPackage.getOrCreateSingletonModules( 365 this@ReactNativeActivity.applicationContext, 366 this@ReactNativeActivity.manifest, 367 extraExpoPackages 368 ) 369 } 370 371 val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion) 372 val builder = versionedUtils.callRecursive( 373 "getReactInstanceManagerBuilder", 374 instanceManagerBuilderProperties 375 ) 376 377 builder.call("setCurrentActivity", this) 378 379 // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here 380 builder.call( 381 "setInitialLifecycleState", 382 RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED") 383 ) 384 385 if (extraNativeModules != null) { 386 for (nativeModule in extraNativeModules) { 387 builder.call("addPackage", nativeModule) 388 } 389 } 390 391 if (delegate.isDebugModeEnabled) { 392 val debuggerHost = manifest!!.getDebuggerHost() 393 val mainModuleName = manifest!!.getMainModuleName() 394 Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder) 395 396 val devLoadingView = 397 RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion) 398 devLoadingView.callRecursive("setDevLoadingEnabled", false) 399 400 val devBundleDownloadListener = 401 RNObject("host.exp.exponent.ExponentDevBundleDownloadListener") 402 .loadVersion(sdkVersion) 403 .construct(progressListener) 404 builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get()) 405 } else { 406 waitForReactAndFinishLoading() 407 } 408 409 val bundle = Bundle() 410 val exponentProps = JSONObject() 411 if (notification != null) { 412 bundle.putString("notification", notification.body) // Deprecated 413 try { 414 exponentProps.put("notification", notification.toJSONObject("selected")) 415 } catch (e: JSONException) { 416 e.printStackTrace() 417 } 418 } 419 420 try { 421 exponentProps.put("manifestString", manifest.toString()) 422 exponentProps.put("shell", isShellApp) 423 exponentProps.put("initialUri", intentUri) 424 } catch (e: JSONException) { 425 EXL.e(TAG, e) 426 } 427 428 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) 429 if (metadata != null) { 430 // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience, 431 // we need to send them with the standard notification events so that you can get all the unread notification through an event 432 // Copy unreadNotifications into exponentProps 433 if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) { 434 try { 435 val unreadNotifications = 436 metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 437 delegate.handleUnreadNotifications(unreadNotifications) 438 } catch (e: JSONException) { 439 e.printStackTrace() 440 } 441 metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 442 } 443 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 444 } 445 446 try { 447 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps)) 448 } catch (e: JSONException) { 449 throw Error("JSONObject failed to be converted to Bundle", e) 450 } 451 452 if (!delegate.isInForeground) { 453 return RNObject("com.facebook.react.ReactInstanceManager") 454 } 455 456 Analytics.markEvent(Analytics.TimedEvent.STARTED_LOADING_REACT_NATIVE) 457 val mReactInstanceManager = builder.callRecursive("build") 458 val devSettings = 459 mReactInstanceManager.callRecursive("getDevSupportManager").callRecursive("getDevSettings") 460 if (devSettings != null) { 461 devSettings.setField("exponentActivityId", activityId) 462 if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) { 463 waitForReactAndFinishLoading() 464 } 465 } 466 467 mReactInstanceManager.onHostResume(this, this) 468 val appKey = manifest!!.getAppKey() 469 reactRootView.call( 470 "startReactApplication", 471 mReactInstanceManager.get(), 472 appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY, 473 initialProps(bundle) 474 ) 475 476 // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager} 477 // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}. 478 // Originally react-native will automatically attach after `startReactApplication`. 479 // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8, 480 // the only remaining path is by `onMeasure`. 481 reactRootView.call("requestLayout") 482 483 return mReactInstanceManager 484 } 485 486 protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean { 487 if (isLoading) { 488 // Don't hit ErrorRecoveryManager until bridge is initialized. 489 // This is the same on iOS. 490 return true 491 } 492 val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!) 493 errorRecoveryManager.markErrored() 494 495 if (!errorRecoveryManager.shouldReloadOnError()) { 496 return true 497 } 498 499 if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) { 500 // Kernel couldn't reload, show error screen 501 return true 502 } 503 504 errorQueue.clear() 505 try { 506 val eventProperties = JSONObject().apply { 507 put(Analytics.USER_ERROR_MESSAGE, errorMessage.userErrorMessage()) 508 put(Analytics.DEVELOPER_ERROR_MESSAGE, errorMessage.developerErrorMessage()) 509 put(Analytics.MANIFEST_URL, manifestUrl) 510 } 511 Analytics.logEvent(Analytics.ERROR_RELOADED, eventProperties) 512 } catch (e: Exception) { 513 EXL.e(TAG, e.message) 514 } 515 516 return false 517 } 518 519 fun onEventMainThread(event: AddedExperienceEventEvent) { 520 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 521 pollForEventsToSendToRN() 522 } 523 } 524 525 fun onEvent(event: ExperienceContentLoaded?) {} 526 527 private fun pollForEventsToSendToRN() { 528 if (manifestUrl == null) { 529 return 530 } 531 532 try { 533 val rctDeviceEventEmitter = 534 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 535 rctDeviceEventEmitter.loadVersion(detachSdkVersion) 536 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext") 537 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 538 if (existingEmitter != null) { 539 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 540 for ((eventName, eventPayload) in events) { 541 existingEmitter.call("emit", eventName, eventPayload) 542 } 543 } 544 } catch (e: Throwable) { 545 EXL.e(TAG, e) 546 } 547 } 548 549 // for getting global permission 550 override fun checkSelfPermission(permission: String): Int { 551 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 552 } 553 554 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 555 // in scoped application we don't have `don't ask again` button 556 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 557 true 558 } else super.shouldShowRequestPermissionRationale(permission) 559 } 560 561 override fun requestPermissions( 562 permissions: Array<String>, 563 requestCode: Int, 564 listener: PermissionListener 565 ) { 566 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 567 val name = manifest!!.getName() 568 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey) 569 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 570 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 571 super.requestPermissions(permissions, requestCode) 572 } 573 } 574 575 override fun onRequestPermissionsResult( 576 requestCode: Int, 577 permissions: Array<String>, 578 grantResults: IntArray 579 ) { 580 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 581 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 582 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 583 scopedPermissionsRequester = null 584 } 585 } 586 } else { 587 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 588 } 589 } 590 591 // for getting scoped permission 592 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 593 val globalResult = super.checkPermission(permission, pid, uid) 594 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 595 globalResult, 596 packageManager, 597 permission, 598 experienceKey!! 599 ) 600 } 601 602 val devSupportManager: RNObject 603 get() = reactInstanceManager.callRecursive("getDevSupportManager") 604 605 // deprecated in favor of Expo.Linking.makeUrl 606 // TODO: remove this 607 private val linkingUri: String? 608 get() = if (Constants.SHELL_APP_SCHEME != null) { 609 Constants.SHELL_APP_SCHEME + "://" 610 } else { 611 val uri = Uri.parse(manifestUrl) 612 val host = uri.host 613 if (host != null && ( 614 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 615 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 616 ".expo.test" 617 ) 618 ) 619 ) { 620 val pathSegments = uri.pathSegments 621 val builder = uri.buildUpon() 622 builder.path(null) 623 for (segment in pathSegments) { 624 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 625 break 626 } 627 builder.appendEncodedPath(segment) 628 } 629 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 630 .toString() 631 } else { 632 manifestUrl 633 } 634 } 635 636 companion object { 637 private val TAG = ReactNativeActivity::class.java.simpleName 638 private const val VIEW_TEST_INTERVAL_MS: Long = 20 639 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 640 } 641 } 642