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.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.ExperienceActivityUtils 43 import host.exp.exponent.utils.ScopedPermissionsRequester 44 import host.exp.expoview.Exponent 45 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties 46 import host.exp.expoview.Exponent.StartReactInstanceDelegate 47 import host.exp.expoview.R 48 import org.json.JSONException 49 import org.json.JSONObject 50 import versioned.host.exp.exponent.ExponentPackage 51 import java.util.* 52 import javax.inject.Inject 53 54 abstract class ReactNativeActivity : 55 AppCompatActivity(), 56 DefaultHardwareBackBtnHandler, 57 PermissionAwareActivity { 58 59 class ExperienceDoneLoadingEvent internal constructor(val activity: Activity) 60 61 open fun initialProps(expBundle: Bundle?): Bundle? { 62 return expBundle 63 } 64 65 protected open fun onDoneLoading() {} 66 67 // Will be called after waitForDrawOverOtherAppPermission 68 protected open fun startReactInstance() {} 69 70 protected var reactInstanceManager: RNObject = 71 RNObject("com.facebook.react.ReactInstanceManager") 72 protected var isCrashed = false 73 74 protected var manifestUrl: String? = null 75 var experienceKey: ExperienceKey? = null 76 protected var sdkVersion: String? = null 77 protected var activityId = 0 78 79 // In detach we want UNVERSIONED most places. We still need the numbered sdk version 80 // when creating cache keys. 81 protected var detachSdkVersion: String? = null 82 83 protected lateinit var reactRootView: RNObject 84 private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer 85 var isLoading = true 86 protected set 87 protected var jsBundlePath: String? = null 88 protected var manifest: Manifest? = null 89 var isInForeground = false 90 protected set 91 private var scopedPermissionsRequester: ScopedPermissionsRequester? = null 92 93 @Inject 94 protected lateinit var exponentSharedPreferences: ExponentSharedPreferences 95 96 @Inject 97 lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry 98 99 private lateinit var containerView: FrameLayout 100 101 /** 102 * This view is optional and available only when the app runs in Expo Go. 103 */ 104 private var loadingView: LoadingView? = null 105 private lateinit var reactContainerView: FrameLayout 106 private val handler = Handler() 107 108 protected open fun shouldCreateLoadingView(): Boolean { 109 return !Constants.isStandaloneApp() || Constants.SHOW_LOADING_VIEW_IN_SHELL_APP 110 } 111 112 val rootView: View? 113 get() = reactRootView.get() as View? 114 115 override fun onCreate(savedInstanceState: Bundle?) { 116 super.onCreate(null) 117 118 containerView = FrameLayout(this) 119 setContentView(containerView) 120 121 reactContainerView = FrameLayout(this) 122 containerView.addView(reactContainerView) 123 124 if (shouldCreateLoadingView()) { 125 containerView.setBackgroundColor( 126 ContextCompat.getColor( 127 this, 128 R.color.splashscreen_background 129 ) 130 ) 131 loadingView = LoadingView(this) 132 loadingView!!.show() 133 containerView.addView(loadingView) 134 } 135 136 doubleTapReloadRecognizer = DoubleTapReloadRecognizer() 137 Exponent.initialize(this, application) 138 NativeModuleDepsProvider.instance.inject(ReactNativeActivity::class.java, this) 139 140 // Can't call this here because subclasses need to do other initialization 141 // before their listener methods are called. 142 // EventBus.getDefault().registerSticky(this); 143 } 144 145 protected fun setReactRootView(reactRootView: View) { 146 reactContainerView.removeAllViews() 147 addReactViewToContentContainer(reactRootView) 148 } 149 150 fun addReactViewToContentContainer(reactView: View) { 151 if (reactView.parent != null) { 152 (reactView.parent as ViewGroup).removeView(reactView) 153 } 154 reactContainerView.addView(reactView) 155 } 156 157 fun hasReactView(reactView: View): Boolean { 158 return reactView.parent === reactContainerView 159 } 160 161 protected fun hideLoadingView() { 162 loadingView?.let { 163 val viewGroup = it.parent as ViewGroup? 164 viewGroup?.removeView(it) 165 it.hide() 166 } 167 loadingView = null 168 } 169 170 protected fun removeAllViewsFromContainer() { 171 containerView.removeAllViews() 172 } 173 // region Loading 174 /** 175 * Successfully finished loading 176 */ 177 @UiThread 178 protected fun finishLoading() { 179 waitForReactAndFinishLoading() 180 } 181 182 /** 183 * There was an error during loading phase 184 */ 185 protected fun interruptLoading() { 186 handler.removeCallbacksAndMessages(null) 187 } 188 189 // Loop until a view is added to the ReactRootView and once it happens run callback 190 private fun waitForReactRootViewToHaveChildrenAndRunCallback(callback: Runnable) { 191 if (reactRootView.isNull) { 192 return 193 } 194 195 if (reactRootView.call("getChildCount") as Int > 0) { 196 callback.run() 197 } else { 198 handler.postDelayed( 199 { waitForReactRootViewToHaveChildrenAndRunCallback(callback) }, 200 VIEW_TEST_INTERVAL_MS 201 ) 202 } 203 } 204 205 /** 206 * Waits for JS side of React to be launched and then performs final launching actions. 207 */ 208 private fun waitForReactAndFinishLoading() { 209 if (Constants.isStandaloneApp() && Constants.SHOW_LOADING_VIEW_IN_SHELL_APP) { 210 val layoutParams = containerView.layoutParams 211 layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT 212 containerView.layoutParams = layoutParams 213 } 214 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: Manifest): Class<out ViewGroup> { 233 val reactRootViewRNClass = reactRootView.rnClass() 234 if (reactRootViewRNClass != null) { 235 return reactRootViewRNClass as Class<out ViewGroup> 236 } 237 var sdkVersion = manifest.getSDKVersion() 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( 358 application = application, 359 jsBundlePath = jsBundlePath, 360 experienceProperties = experienceProperties, 361 expoPackages = extraExpoPackages, 362 exponentPackageDelegate = delegate.exponentPackageDelegate, 363 manifest = manifest!!, 364 singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages) 365 ) 366 367 val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!) 368 val builder = versionedUtils.callRecursive( 369 "getReactInstanceManagerBuilder", 370 instanceManagerBuilderProperties 371 )!! 372 373 builder.call("setCurrentActivity", this) 374 375 // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here 376 builder.call( 377 "setInitialLifecycleState", 378 RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED") 379 ) 380 381 if (extraNativeModules != null) { 382 for (nativeModule in extraNativeModules) { 383 builder.call("addPackage", nativeModule) 384 } 385 } 386 387 if (delegate.isDebugModeEnabled) { 388 val debuggerHost = manifest!!.getDebuggerHost() 389 val mainModuleName = manifest!!.getMainModuleName() 390 Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder) 391 392 val devLoadingView = 393 RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion) 394 devLoadingView.callRecursive("setDevLoadingEnabled", false) 395 396 val devBundleDownloadListener = 397 RNObject("host.exp.exponent.ExponentDevBundleDownloadListener") 398 .loadVersion(sdkVersion) 399 .construct(progressListener) 400 builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get()) 401 } else { 402 waitForReactAndFinishLoading() 403 } 404 405 val bundle = Bundle() 406 val exponentProps = JSONObject() 407 if (notification != null) { 408 bundle.putString("notification", notification.body) // Deprecated 409 try { 410 exponentProps.put("notification", notification.toJSONObject("selected")) 411 } catch (e: JSONException) { 412 e.printStackTrace() 413 } 414 } 415 416 try { 417 exponentProps.put("manifestString", manifest.toString()) 418 exponentProps.put("shell", isShellApp) 419 exponentProps.put("initialUri", intentUri) 420 } catch (e: JSONException) { 421 EXL.e(TAG, e) 422 } 423 424 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!) 425 if (metadata != null) { 426 // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience, 427 // we need to send them with the standard notification events so that you can get all the unread notification through an event 428 // Copy unreadNotifications into exponentProps 429 if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) { 430 try { 431 val unreadNotifications = 432 metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 433 delegate.handleUnreadNotifications(unreadNotifications) 434 } catch (e: JSONException) { 435 e.printStackTrace() 436 } 437 metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS) 438 } 439 exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata) 440 } 441 442 try { 443 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps)) 444 } catch (e: JSONException) { 445 throw Error("JSONObject failed to be converted to Bundle", e) 446 } 447 448 if (!delegate.isInForeground) { 449 return RNObject("com.facebook.react.ReactInstanceManager") 450 } 451 452 Analytics.markEvent(Analytics.TimedEvent.STARTED_LOADING_REACT_NATIVE) 453 val mReactInstanceManager = builder.callRecursive("build")!! 454 val devSettings = 455 mReactInstanceManager.callRecursive("getDevSupportManager")!!.callRecursive("getDevSettings") 456 if (devSettings != null) { 457 devSettings.setField("exponentActivityId", activityId) 458 if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) { 459 waitForReactAndFinishLoading() 460 } 461 } 462 463 mReactInstanceManager.onHostResume(this, this) 464 val appKey = manifest!!.getAppKey() 465 reactRootView.call( 466 "startReactApplication", 467 mReactInstanceManager.get(), 468 appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY, 469 initialProps(bundle) 470 ) 471 472 // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager} 473 // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}. 474 // Originally react-native will automatically attach after `startReactApplication`. 475 // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8, 476 // the only remaining path is by `onMeasure`. 477 reactRootView.call("requestLayout") 478 479 return mReactInstanceManager 480 } 481 482 protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean { 483 if (isLoading) { 484 // Don't hit ErrorRecoveryManager until bridge is initialized. 485 // This is the same on iOS. 486 return true 487 } 488 val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!) 489 errorRecoveryManager.markErrored() 490 491 if (!errorRecoveryManager.shouldReloadOnError()) { 492 return true 493 } 494 495 if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) { 496 // Kernel couldn't reload, show error screen 497 return true 498 } 499 500 errorQueue.clear() 501 try { 502 val eventProperties = JSONObject().apply { 503 put(Analytics.USER_ERROR_MESSAGE, errorMessage.userErrorMessage()) 504 put(Analytics.DEVELOPER_ERROR_MESSAGE, errorMessage.developerErrorMessage()) 505 put(Analytics.MANIFEST_URL, manifestUrl) 506 } 507 Analytics.logEvent(Analytics.AnalyticsEvent.ERROR_RELOADED, eventProperties) 508 } catch (e: Exception) { 509 EXL.e(TAG, e.message) 510 } 511 512 return false 513 } 514 515 fun onEventMainThread(event: AddedExperienceEventEvent) { 516 if (manifestUrl != null && manifestUrl == event.manifestUrl) { 517 pollForEventsToSendToRN() 518 } 519 } 520 521 fun onEvent(event: ExperienceContentLoaded?) {} 522 523 private fun pollForEventsToSendToRN() { 524 if (manifestUrl == null) { 525 return 526 } 527 528 try { 529 val rctDeviceEventEmitter = 530 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 531 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 532 val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!! 533 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 534 if (existingEmitter != null) { 535 val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!) 536 for ((eventName, eventPayload) in events) { 537 existingEmitter.call("emit", eventName, eventPayload) 538 } 539 } 540 } catch (e: Throwable) { 541 EXL.e(TAG, e) 542 } 543 } 544 545 // for getting global permission 546 override fun checkSelfPermission(permission: String): Int { 547 return super.checkPermission(permission, Process.myPid(), Process.myUid()) 548 } 549 550 override fun shouldShowRequestPermissionRationale(permission: String): Boolean { 551 // in scoped application we don't have `don't ask again` button 552 return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { 553 true 554 } else super.shouldShowRequestPermissionRationale(permission) 555 } 556 557 override fun requestPermissions( 558 permissions: Array<String>, 559 requestCode: Int, 560 listener: PermissionListener 561 ) { 562 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 563 val name = manifest!!.getName() 564 scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!) 565 scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener) 566 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 567 super.requestPermissions(permissions, requestCode) 568 } 569 } 570 571 override fun onRequestPermissionsResult( 572 requestCode: Int, 573 permissions: Array<String>, 574 grantResults: IntArray 575 ) { 576 if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) { 577 if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) { 578 if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) { 579 scopedPermissionsRequester = null 580 } 581 } 582 } else { 583 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 584 } 585 } 586 587 // for getting scoped permission 588 override fun checkPermission(permission: String, pid: Int, uid: Int): Int { 589 val globalResult = super.checkPermission(permission, pid, uid) 590 return expoKernelServiceRegistry.permissionsKernelService.getPermissions( 591 globalResult, 592 packageManager, 593 permission, 594 experienceKey!! 595 ) 596 } 597 598 val devSupportManager: RNObject 599 get() = reactInstanceManager.callRecursive("getDevSupportManager")!! 600 601 // deprecated in favor of Expo.Linking.makeUrl 602 // TODO: remove this 603 private val linkingUri: String? 604 get() = if (Constants.SHELL_APP_SCHEME != null) { 605 Constants.SHELL_APP_SCHEME + "://" 606 } else { 607 val uri = Uri.parse(manifestUrl) 608 val host = uri.host 609 if (host != null && ( 610 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 611 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 612 ".expo.test" 613 ) 614 ) 615 ) { 616 val pathSegments = uri.pathSegments 617 val builder = uri.buildUpon() 618 builder.path(null) 619 for (segment in pathSegments) { 620 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 621 break 622 } 623 builder.appendEncodedPath(segment) 624 } 625 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 626 .toString() 627 } else { 628 manifestUrl 629 } 630 } 631 632 companion object { 633 private val TAG = ReactNativeActivity::class.java.simpleName 634 private const val VIEW_TEST_INTERVAL_MS: Long = 20 635 @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList() 636 } 637 } 638