1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent.experience 3 4 import android.app.AlertDialog 5 import android.app.Notification 6 import android.app.NotificationManager 7 import android.app.PendingIntent 8 import android.content.Context 9 import android.content.Intent 10 import android.net.Uri 11 import android.os.Bundle 12 import android.text.TextUtils 13 import android.view.KeyEvent 14 import android.view.View 15 import android.view.ViewGroup 16 import android.view.animation.AccelerateInterpolator 17 import android.view.animation.AlphaAnimation 18 import android.view.animation.Animation 19 import android.widget.RemoteViews 20 import androidx.core.app.NotificationCompat 21 import androidx.core.content.ContextCompat 22 import com.facebook.react.ReactPackage 23 import com.facebook.react.bridge.UiThreadUtil 24 import com.facebook.soloader.SoLoader 25 import de.greenrobot.event.EventBus 26 import expo.modules.core.interfaces.Package 27 import expo.modules.splashscreen.singletons.SplashScreen 28 import expo.modules.manifests.core.Manifest 29 import host.exp.exponent.* 30 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 31 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 32 import host.exp.exponent.analytics.Analytics 33 import host.exp.exponent.analytics.EXL 34 import host.exp.exponent.branch.BranchManager 35 import host.exp.exponent.di.NativeModuleDepsProvider 36 import host.exp.exponent.experience.loading.LoadingProgressPopupController 37 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenConfiguration 38 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewController 39 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewProvider 40 import host.exp.exponent.kernel.* 41 import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent 42 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions 43 import host.exp.exponent.notifications.* 44 import host.exp.exponent.storage.ExponentDB 45 import host.exp.exponent.storage.ExponentDBObject 46 import host.exp.exponent.utils.AsyncCondition 47 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener 48 import host.exp.exponent.utils.ExperienceActivityUtils 49 import host.exp.exponent.utils.ExpoActivityIds 50 import host.exp.expoview.Exponent 51 import host.exp.expoview.Exponent.StartReactInstanceDelegate 52 import host.exp.expoview.R 53 import org.json.JSONArray 54 import org.json.JSONException 55 import org.json.JSONObject 56 import versioned.host.exp.exponent.ExponentPackageDelegate 57 import versioned.host.exp.exponent.ReactUnthemedRootView 58 import java.lang.ref.WeakReference 59 import javax.inject.Inject 60 61 open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDelegate { 62 open fun expoPackages(): List<Package>? { 63 // Experience must pick its own modules in ExponentPackage 64 return null 65 } 66 67 open fun reactPackages(): List<ReactPackage>? { 68 return null 69 } 70 71 override val exponentPackageDelegate: ExponentPackageDelegate? = null 72 73 private var nuxOverlayView: ReactUnthemedRootView? = null 74 private var notification: ExponentNotification? = null 75 private var tempNotification: ExponentNotification? = null 76 private var isShellApp = false 77 protected var intentUri: String? = null 78 private var isReadyForBundle = false 79 private var notificationRemoteViews: RemoteViews? = null 80 private var notificationBuilder: NotificationCompat.Builder? = null 81 private var isLoadExperienceAllowedToRun = false 82 private var shouldShowLoadingViewWithOptimisticManifest = false 83 84 /** 85 * Controls loadingProgressPopupWindow that is shown above whole activity. 86 */ 87 lateinit var loadingProgressPopupController: LoadingProgressPopupController 88 var managedAppSplashScreenViewProvider: ManagedAppSplashScreenViewProvider? = null 89 90 @Inject 91 lateinit var exponentManifest: ExponentManifest 92 93 @Inject 94 lateinit var devMenuManager: DevMenuManager 95 96 private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener = 97 object : DevBundleDownloadProgressListener { 98 override fun onProgress(status: String?, done: Int?, total: Int?) { 99 UiThreadUtil.runOnUiThread { 100 loadingProgressPopupController.updateProgress( 101 status, 102 done, 103 total 104 ) 105 } 106 } 107 108 override fun onSuccess() { 109 UiThreadUtil.runOnUiThread { 110 loadingProgressPopupController.hide() 111 finishLoading() 112 } 113 } 114 115 override fun onFailure(error: Exception) { 116 UiThreadUtil.runOnUiThread { 117 loadingProgressPopupController.hide() 118 interruptLoading() 119 } 120 } 121 } 122 123 /* 124 * 125 * Lifecycle 126 * 127 */ 128 override fun onCreate(savedInstanceState: Bundle?) { 129 super.onCreate(savedInstanceState) 130 131 isLoadExperienceAllowedToRun = true 132 shouldShowLoadingViewWithOptimisticManifest = true 133 loadingProgressPopupController = LoadingProgressPopupController(this) 134 135 NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this) 136 EventBus.getDefault().registerSticky(this) 137 138 activityId = ExpoActivityIds.getNextAppActivityId() 139 140 // TODO: audit this now that kernel logic is on the native side in Kotlin 141 var shouldOpenImmediately = true 142 143 // If our activity was killed for memory reasons or because of "Don't keep activities", 144 // try to reload manifest using the savedInstanceState 145 if (savedInstanceState != null) { 146 val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY) 147 if (manifestUrl != null) { 148 this.manifestUrl = manifestUrl 149 } 150 } 151 152 // On cold boot to experience, we're given this information from the Kotlin kernel, instead of 153 // the JS kernel. 154 val bundle = intent.extras 155 if (bundle != null && this.manifestUrl == null) { 156 val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY) 157 if (manifestUrl != null) { 158 this.manifestUrl = manifestUrl 159 } 160 161 // Don't want to get here if savedInstanceState has manifestUrl. Only care about 162 // IS_OPTIMISTIC_KEY the first time onCreate is called. 163 val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY) 164 if (isOptimistic) { 165 shouldOpenImmediately = false 166 } 167 } 168 169 if (this.manifestUrl != null && shouldOpenImmediately) { 170 val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false) 171 ExpoUpdatesAppLoader( 172 this.manifestUrl!!, 173 object : AppLoaderCallback { 174 override fun onOptimisticManifest(optimisticManifest: Manifest) { 175 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 176 } 177 178 override fun onManifestCompleted(manifest: Manifest) { 179 Exponent.instance.runOnUiThread { 180 try { 181 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 182 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl) 183 } catch (e: JSONException) { 184 kernel.handleError(e) 185 } 186 } 187 } 188 189 override fun onBundleCompleted(localBundlePath: String?) { 190 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 191 } 192 193 override fun emitEvent(params: JSONObject) { 194 emitUpdatesEvent(params) 195 } 196 197 override fun updateStatus(status: AppLoaderStatus) { 198 setLoadingProgressStatusIfEnabled(status) 199 } 200 201 override fun onError(e: Exception) { 202 Exponent.instance.runOnUiThread { kernel.handleError(e) } 203 } 204 }, 205 forceCache 206 ).start(this) 207 } 208 kernel.setOptimisticActivity(this, taskId) 209 } 210 211 override fun onResume() { 212 super.onResume() 213 currentActivity = this 214 215 // Resume home's host if needed. 216 devMenuManager.maybeResumeHostWithActivity(this) 217 218 soLoaderInit() 219 220 addNotification() 221 Analytics.logEventWithManifestUrl(Analytics.AnalyticsEvent.EXPERIENCE_APPEARED, manifestUrl) 222 } 223 224 override fun onWindowFocusChanged(hasFocus: Boolean) { 225 super.onWindowFocusChanged(hasFocus) 226 // Check for manifest to avoid calling this when first loading an experience 227 if (hasFocus && manifest != null) { 228 runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) } 229 } 230 } 231 232 private fun soLoaderInit() { 233 if (detachSdkVersion != null) { 234 SoLoader.init(this, false) 235 } 236 } 237 238 open fun shouldCheckOptions() { 239 if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) { 240 handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!) 241 } 242 } 243 244 override fun onPause() { 245 super.onPause() 246 if (currentActivity === this) { 247 currentActivity = null 248 } 249 removeNotification() 250 Analytics.clearTimedEvents() 251 } 252 253 public override fun onSaveInstanceState(savedInstanceState: Bundle) { 254 savedInstanceState.putString(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 255 super.onSaveInstanceState(savedInstanceState) 256 } 257 258 override fun onNewIntent(intent: Intent) { 259 super.onNewIntent(intent) 260 val uri = intent.data 261 if (uri != null) { 262 handleUri(uri.toString()) 263 } 264 } 265 266 fun toggleDevMenu(): Boolean { 267 if (reactInstanceManager.isNotNull && !isCrashed) { 268 devMenuManager.toggleInActivity(this) 269 return true 270 } 271 return false 272 } 273 274 /** 275 * Handles command line command `adb shell input keyevent 82` that toggles the dev menu on the current experience activity. 276 */ 277 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 278 if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager.isNotNull && !isCrashed) { 279 devMenuManager.toggleInActivity(this) 280 return true 281 } 282 return super.onKeyUp(keyCode, event) 283 } 284 285 /** 286 * Closes the dev menu when pressing back button when it is visible on this activity. 287 */ 288 override fun onBackPressed() { 289 if (currentActivity === this && devMenuManager.isShownInActivity(this)) { 290 devMenuManager.requestToClose(this) 291 return 292 } 293 super.onBackPressed() 294 } 295 296 fun onEventMainThread(event: KernelStartedRunningEvent?) { 297 AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY) 298 } 299 300 override fun onDoneLoading() { 301 Analytics.markEvent(Analytics.TimedEvent.FINISHED_LOADING_REACT_NATIVE) 302 Analytics.sendTimedEvents(manifestUrl) 303 } 304 305 fun onEvent(event: ExperienceDoneLoadingEvent) { 306 if (event.activity === this) { 307 loadingProgressPopupController.hide() 308 } 309 310 if (!Constants.isStandaloneApp()) { 311 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 312 if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) { 313 AlertDialog.Builder(this@ExperienceActivity) 314 .setTitle("Using a cached project") 315 .setMessage("Expo was unable to fetch the latest update to this app. A previously downloaded version has been launched. If you did not intend to use a cached project, check your network connection and reload the app.") 316 .setPositiveButton("Use cache", null) 317 .setNegativeButton("Reload") { _, _ -> 318 kernel.reloadVisibleExperience( 319 manifestUrl!!, false 320 ) 321 } 322 .show() 323 } 324 } 325 } 326 327 /* 328 * 329 * Experience Loading 330 * 331 */ 332 fun startLoading() { 333 isLoading = true 334 showOrReconfigureManagedAppSplashScreen(manifest) 335 setLoadingProgressStatusIfEnabled() 336 } 337 338 /** 339 * This method is being called twice: 340 * - first time for optimistic manifest 341 * - seconds time for real manifest 342 */ 343 protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) { 344 if (!shouldCreateLoadingView()) { 345 return 346 } 347 348 hideLoadingView() 349 if (managedAppSplashScreenViewProvider == null) { 350 val config = ManagedAppSplashScreenConfiguration.parseManifest( 351 manifest!! 352 ) 353 managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config) 354 val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this) 355 val controller = ManagedAppSplashScreenViewController( 356 this, 357 getRootViewClass( 358 manifest 359 ), 360 splashScreenView 361 ) 362 SplashScreen.show(this, controller, true) 363 } else { 364 managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!) 365 } 366 } 367 368 fun setLoadingProgressStatusIfEnabled() { 369 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 370 if (appLoader != null) { 371 setLoadingProgressStatusIfEnabled(appLoader.status) 372 } 373 } 374 375 fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) { 376 if (Constants.isStandaloneApp()) { 377 return 378 } 379 if (status == null) { 380 return 381 } 382 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 383 if (appLoader != null && appLoader.shouldShowAppLoaderStatus) { 384 UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) } 385 } else { 386 UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() } 387 } 388 } 389 390 fun setOptimisticManifest(optimisticManifest: Manifest) { 391 runOnUiThread { 392 if (!isInForeground) { 393 return@runOnUiThread 394 } 395 if (!shouldShowLoadingViewWithOptimisticManifest) { 396 return@runOnUiThread 397 } 398 ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity) 399 ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity) 400 ExperienceActivityUtils.setTaskDescription( 401 exponentManifest, 402 optimisticManifest, 403 this@ExperienceActivity 404 ) 405 showOrReconfigureManagedAppSplashScreen(optimisticManifest) 406 setLoadingProgressStatusIfEnabled() 407 } 408 } 409 410 fun setManifest( 411 manifestUrl: String, 412 manifest: Manifest, 413 bundleUrl: String 414 ) { 415 if (!isInForeground) { 416 return 417 } 418 if (!isLoadExperienceAllowedToRun) { 419 return 420 } 421 422 // Only want to run once per onCreate. There are some instances with ShellAppActivity where this would be called 423 // twice otherwise. Turn on "Don't keep activities", trigger a notification, background the app, and then 424 // press on the notification in a shell app to see this happen. 425 isLoadExperienceAllowedToRun = false 426 427 isReadyForBundle = false 428 this.manifestUrl = manifestUrl 429 this.manifest = manifest 430 431 exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!) 432 433 // Notifications logic uses this to determine which experience to route a notification to 434 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl)) 435 436 ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!) 437 438 val task = kernel.getExperienceActivityTask(this.manifestUrl!!) 439 task.taskId = taskId 440 task.experienceActivity = WeakReference(this) 441 task.activityId = activityId 442 task.bundleUrl = bundleUrl 443 444 sdkVersion = manifest.getSDKVersion() 445 isShellApp = this.manifestUrl == Constants.INITIAL_URL 446 447 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 448 // to point to the unversioned code in ReactAndroid. 449 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 450 sdkVersion = RNObject.UNVERSIONED 451 } 452 453 // In detach/shell, we always use UNVERSIONED as the ABI. 454 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 455 456 if (RNObject.UNVERSIONED != sdkVersion) { 457 var isValidVersion = false 458 for (version in Constants.SDK_VERSIONS_LIST) { 459 if (version == sdkVersion) { 460 isValidVersion = true 461 break 462 } 463 } 464 if (!isValidVersion) { 465 KernelProvider.instance.handleError( 466 sdkVersion + " is not a valid SDK version. Options are " + 467 TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "." 468 ) 469 return 470 } 471 } 472 473 soLoaderInit() 474 475 try { 476 experienceKey = ExperienceKey.fromManifest(manifest) 477 AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY) 478 } catch (e: JSONException) { 479 KernelProvider.instance.handleError("No ID found in manifest.") 480 return 481 } 482 483 isCrashed = false 484 485 Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion) 486 487 ExperienceActivityUtils.updateOrientation(this.manifest!!, this) 488 ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest!!, this) 489 ExperienceActivityUtils.overrideUiMode(this.manifest!!, this) 490 491 addNotification() 492 493 var notificationObject: ExponentNotification? = null 494 // Activity could be restarted due to Dark Mode change, only pop options if that will not happen 495 if (kernel.hasOptionsForManifestUrl(manifestUrl)) { 496 val options = kernel.popOptionsForManifestUrl(manifestUrl) 497 498 // if the kernel has an intent for our manifest url, that's the intent that triggered 499 // the loading of this experience. 500 if (options!!.uri != null) { 501 intentUri = options.uri 502 } 503 notificationObject = options.notificationObject 504 } 505 506 BranchManager.handleLink(this, intentUri, detachSdkVersion) 507 508 runOnUiThread { 509 if (!isInForeground) { 510 return@runOnUiThread 511 } 512 if (reactInstanceManager.isNotNull) { 513 reactInstanceManager.onHostDestroy() 514 reactInstanceManager.assign(null) 515 } 516 517 reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView") 518 reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity) 519 setReactRootView((reactRootView.get() as View)) 520 521 if (isDebugModeEnabled) { 522 notification = notificationObject 523 jsBundlePath = "" 524 startReactInstance() 525 } else { 526 tempNotification = notificationObject 527 isReadyForBundle = true 528 AsyncCondition.notify(READY_FOR_BUNDLE) 529 } 530 531 ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity) 532 ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity) 533 ExperienceActivityUtils.setTaskDescription( 534 exponentManifest, 535 manifest, 536 this@ExperienceActivity 537 ) 538 showOrReconfigureManagedAppSplashScreen(manifest) 539 setLoadingProgressStatusIfEnabled() 540 } 541 } 542 543 fun setBundle(localBundlePath: String?) { 544 // by this point, setManifest should have also been called, so prevent 545 // setOptimisticManifest from showing a rogue splash screen 546 shouldShowLoadingViewWithOptimisticManifest = false 547 if (!isDebugModeEnabled) { 548 val finalIsReadyForBundle = isReadyForBundle 549 AsyncCondition.wait( 550 READY_FOR_BUNDLE, 551 object : AsyncConditionListener { 552 override fun isReady(): Boolean { 553 return finalIsReadyForBundle 554 } 555 556 override fun execute() { 557 notification = tempNotification 558 tempNotification = null 559 jsBundlePath = localBundlePath 560 startReactInstance() 561 AsyncCondition.remove(READY_FOR_BUNDLE) 562 } 563 } 564 ) 565 } 566 } 567 568 fun onEventMainThread(event: ReceivedNotificationEvent) { 569 // TODO(wschurman): investigate removal, this probably is no longer used 570 if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) { 571 try { 572 val rctDeviceEventEmitter = 573 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 574 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 575 reactInstanceManager.callRecursive("getCurrentReactContext")!! 576 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 577 .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received")) 578 } catch (e: Throwable) { 579 EXL.e(TAG, e) 580 } 581 } 582 } 583 584 fun handleOptions(options: ExperienceOptions) { 585 try { 586 val uri = options.uri 587 if (uri !== null) { 588 handleUri(uri) 589 val rctDeviceEventEmitter = 590 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 591 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 592 reactInstanceManager.callRecursive("getCurrentReactContext")!! 593 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 594 .call("emit", "Exponent.openUri", uri) 595 BranchManager.handleLink(this, uri, detachSdkVersion) 596 } 597 if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) { 598 val rctDeviceEventEmitter = 599 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 600 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 601 reactInstanceManager.callRecursive("getCurrentReactContext")!! 602 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 603 .call( 604 "emit", 605 "Exponent.notification", 606 options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected") 607 ) 608 } 609 } catch (e: Throwable) { 610 EXL.e(TAG, e) 611 } 612 } 613 614 private fun handleUri(uri: String) { 615 // Emits a "url" event to the Linking event emitter 616 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) 617 super.onNewIntent(intent) 618 } 619 620 fun emitUpdatesEvent(params: JSONObject) { 621 KernelProvider.instance.addEventForExperience( 622 manifestUrl!!, 623 KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString()) 624 ) 625 } 626 627 override val isDebugModeEnabled: Boolean 628 get() = manifest?.isDevelopmentMode() ?: false 629 630 override fun startReactInstance() { 631 Exponent.instance 632 .testPackagerStatus( 633 isDebugModeEnabled, manifest!!, 634 object : Exponent.PackagerStatusCallback { 635 override fun onSuccess() { 636 reactInstanceManager = startReactInstance( 637 this@ExperienceActivity, 638 intentUri, 639 detachSdkVersion, 640 notification, 641 isShellApp, 642 reactPackages(), 643 expoPackages(), 644 devBundleDownloadProgressListener 645 ) 646 } 647 648 override fun onFailure(errorMessage: String) { 649 KernelProvider.instance.handleError(errorMessage) 650 } 651 } 652 ) 653 } 654 655 override fun handleUnreadNotifications(unreadNotifications: JSONArray) { 656 PushNotificationHelper.instance.removeNotifications(this, unreadNotifications) 657 } 658 659 /* 660 * 661 * Notification 662 * 663 */ 664 private fun addNotification() { 665 if (isShellApp || manifestUrl == null || manifest == null) { 666 return 667 } 668 669 val name = manifest!!.getName() ?: return 670 671 val remoteViews = RemoteViews(packageName, R.layout.notification) 672 remoteViews.setCharSequence(R.id.home_text_button, "setText", name) 673 674 // Home 675 val homeIntent = Intent(this, LauncherActivity::class.java) 676 remoteViews.setOnClickPendingIntent( 677 R.id.home_image_button, 678 PendingIntent.getActivity( 679 this, 0, 680 homeIntent, 0 681 ) 682 ) 683 684 // Reload 685 remoteViews.setOnClickPendingIntent( 686 R.id.reload_button, 687 PendingIntent.getService( 688 this, 0, 689 ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT 690 ) 691 ) 692 693 notificationRemoteViews = remoteViews 694 695 // Build the actual notification 696 val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 697 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 698 699 ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel() 700 notificationBuilder = 701 NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID) 702 .setContent(notificationRemoteViews) 703 .setSmallIcon(R.drawable.notification_icon) 704 .setShowWhen(false) 705 .setOngoing(true) 706 .setPriority(Notification.PRIORITY_MAX) 707 .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) 708 709 notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build()) 710 } 711 712 fun removeNotification() { 713 notificationRemoteViews = null 714 notificationBuilder = null 715 removeNotification(this) 716 } 717 718 fun onNotificationAction() { 719 dismissNuxViewIfVisible(true) 720 } 721 722 /** 723 * @param isFromNotification true if this is the result of the user taking an 724 * action in the notification view. 725 */ 726 fun dismissNuxViewIfVisible(isFromNotification: Boolean) { 727 if (nuxOverlayView == null) { 728 return 729 } 730 731 runOnUiThread { 732 val fadeOut: Animation = AlphaAnimation(1f, 0f).apply { 733 interpolator = AccelerateInterpolator() 734 duration = 500 735 setAnimationListener(object : Animation.AnimationListener { 736 override fun onAnimationEnd(animation: Animation) { 737 if (nuxOverlayView!!.parent != null) { 738 (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView) 739 } 740 nuxOverlayView = null 741 val eventProperties = JSONObject() 742 try { 743 eventProperties.put("IS_FROM_NOTIFICATION", isFromNotification) 744 } catch (e: JSONException) { 745 EXL.e(TAG, e.message) 746 } 747 Analytics.logEvent(Analytics.AnalyticsEvent.NUX_EXPERIENCE_OVERLAY_DISMISSED, eventProperties) 748 } 749 750 override fun onAnimationRepeat(animation: Animation) {} 751 override fun onAnimationStart(animation: Animation) {} 752 }) 753 } 754 nuxOverlayView!!.startAnimation(fadeOut) 755 } 756 } 757 758 /* 759 * 760 * Errors 761 * 762 */ 763 override fun onError(intent: Intent) { 764 if (manifestUrl != null) { 765 intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl) 766 } 767 } 768 769 companion object { 770 private val TAG = ExperienceActivity::class.java.simpleName 771 private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" 772 const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 773 private const val READY_FOR_BUNDLE = "readyForBundle" 774 775 /** 776 * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user. 777 */ 778 var currentActivity: ExperienceActivity? = null 779 private set 780 781 @JvmStatic fun removeNotification(context: Context) { 782 val notificationManager = 783 context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 784 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 785 } 786 } 787 } 788