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.updates.manifest.raw.RawManifest 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.getInstance().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: RawManifest) { 175 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 176 } 177 178 override fun onManifestCompleted(manifest: RawManifest) { 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.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: RawManifest?) { 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: RawManifest) { 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: RawManifest, 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 // TODO(eric): remove when deleting old AppLoader class/logic 432 exponentSharedPreferences.updateManifest(this.manifestUrl, manifest, bundleUrl) 433 434 // Notifications logic uses this to determine which experience to route a notification to 435 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl)) 436 437 ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest) 438 439 val task = kernel.getExperienceActivityTask(this.manifestUrl!!) 440 task.taskId = taskId 441 task.experienceActivity = WeakReference(this) 442 task.activityId = activityId 443 task.bundleUrl = bundleUrl 444 445 sdkVersion = manifest.getSDKVersionNullable() 446 isShellApp = this.manifestUrl == Constants.INITIAL_URL 447 448 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 449 // to point to the unversioned code in ReactAndroid. 450 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 451 sdkVersion = RNObject.UNVERSIONED 452 } 453 454 // In detach/shell, we always use UNVERSIONED as the ABI. 455 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 456 457 if (RNObject.UNVERSIONED != sdkVersion) { 458 var isValidVersion = false 459 for (version in Constants.SDK_VERSIONS_LIST) { 460 if (version == sdkVersion) { 461 isValidVersion = true 462 break 463 } 464 } 465 if (!isValidVersion) { 466 KernelProvider.instance.handleError( 467 sdkVersion + " is not a valid SDK version. Options are " + 468 TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "." 469 ) 470 return 471 } 472 } 473 474 soLoaderInit() 475 476 try { 477 experienceKey = ExperienceKey.fromRawManifest(manifest) 478 AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY) 479 } catch (e: JSONException) { 480 KernelProvider.instance.handleError("No ID found in manifest.") 481 return 482 } 483 484 isCrashed = false 485 486 Analytics.logEventWithManifestUrlSdkVersion(Analytics.LOAD_EXPERIENCE, manifestUrl, sdkVersion) 487 488 ExperienceActivityUtils.updateOrientation(this.manifest, this) 489 ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest, this) 490 ExperienceActivityUtils.overrideUiMode(this.manifest, this) 491 492 addNotification() 493 494 var notificationObject: ExponentNotification? = null 495 // Activity could be restarted due to Dark Mode change, only pop options if that will not happen 496 if (kernel.hasOptionsForManifestUrl(manifestUrl)) { 497 val options = kernel.popOptionsForManifestUrl(manifestUrl) 498 499 // if the kernel has an intent for our manifest url, that's the intent that triggered 500 // the loading of this experience. 501 if (options!!.uri != null) { 502 intentUri = options.uri 503 } 504 notificationObject = options.notificationObject 505 } 506 507 BranchManager.handleLink(this, intentUri, detachSdkVersion) 508 509 runOnUiThread { 510 if (!isInForeground) { 511 return@runOnUiThread 512 } 513 if (reactInstanceManager.isNotNull) { 514 reactInstanceManager.onHostDestroy() 515 reactInstanceManager.assign(null) 516 } 517 518 reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView") 519 reactRootView.loadVersion(detachSdkVersion).construct(this@ExperienceActivity) 520 setReactRootView((reactRootView.get() as View)) 521 522 if (isDebugModeEnabled) { 523 notification = notificationObject 524 jsBundlePath = "" 525 startReactInstance() 526 } else { 527 tempNotification = notificationObject 528 isReadyForBundle = true 529 AsyncCondition.notify(READY_FOR_BUNDLE) 530 } 531 532 ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity) 533 ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity) 534 ExperienceActivityUtils.setTaskDescription( 535 exponentManifest, 536 manifest, 537 this@ExperienceActivity 538 ) 539 showOrReconfigureManagedAppSplashScreen(manifest) 540 setLoadingProgressStatusIfEnabled() 541 } 542 } 543 544 fun setBundle(localBundlePath: String?) { 545 // by this point, setManifest should have also been called, so prevent 546 // setOptimisticManifest from showing a rogue splash screen 547 shouldShowLoadingViewWithOptimisticManifest = false 548 if (!isDebugModeEnabled) { 549 val finalIsReadyForBundle = isReadyForBundle 550 AsyncCondition.wait( 551 READY_FOR_BUNDLE, 552 object : AsyncConditionListener { 553 override fun isReady(): Boolean { 554 return finalIsReadyForBundle 555 } 556 557 override fun execute() { 558 notification = tempNotification 559 tempNotification = null 560 jsBundlePath = localBundlePath 561 startReactInstance() 562 AsyncCondition.remove(READY_FOR_BUNDLE) 563 } 564 } 565 ) 566 } 567 } 568 569 fun onEventMainThread(event: ReceivedNotificationEvent) { 570 // TODO(wschurman): investigate removal, this probably is no longer used 571 if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) { 572 try { 573 val rctDeviceEventEmitter = 574 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 575 rctDeviceEventEmitter.loadVersion(detachSdkVersion) 576 reactInstanceManager.callRecursive("getCurrentReactContext") 577 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 578 .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received")) 579 } catch (e: Throwable) { 580 EXL.e(TAG, e) 581 } 582 } 583 } 584 585 fun handleOptions(options: ExperienceOptions) { 586 try { 587 val uri = options.uri 588 if (uri !== null) { 589 handleUri(uri) 590 val rctDeviceEventEmitter = 591 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 592 rctDeviceEventEmitter.loadVersion(detachSdkVersion) 593 reactInstanceManager.callRecursive("getCurrentReactContext") 594 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 595 .call("emit", "Exponent.openUri", uri) 596 BranchManager.handleLink(this, uri, detachSdkVersion) 597 } 598 if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) { 599 val rctDeviceEventEmitter = 600 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 601 rctDeviceEventEmitter.loadVersion(detachSdkVersion) 602 reactInstanceManager.callRecursive("getCurrentReactContext") 603 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass()) 604 .call( 605 "emit", 606 "Exponent.notification", 607 options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected") 608 ) 609 } 610 } catch (e: Throwable) { 611 EXL.e(TAG, e) 612 } 613 } 614 615 private fun handleUri(uri: String) { 616 // Emits a "url" event to the Linking event emitter 617 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) 618 super.onNewIntent(intent) 619 } 620 621 fun emitUpdatesEvent(params: JSONObject) { 622 KernelProvider.instance.addEventForExperience( 623 manifestUrl!!, 624 KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString()) 625 ) 626 } 627 628 override val isDebugModeEnabled: Boolean 629 get() = manifest?.isDevelopmentMode() ?: false 630 631 override fun startReactInstance() { 632 Exponent.instance 633 .testPackagerStatus( 634 isDebugModeEnabled, manifest!!, 635 object : Exponent.PackagerStatusCallback { 636 override fun onSuccess() { 637 reactInstanceManager = startReactInstance( 638 this@ExperienceActivity, 639 intentUri, 640 detachSdkVersion, 641 notification, 642 isShellApp, 643 reactPackages(), 644 expoPackages(), 645 devBundleDownloadProgressListener 646 ) 647 } 648 649 override fun onFailure(errorMessage: String) { 650 KernelProvider.instance.handleError(errorMessage) 651 } 652 } 653 ) 654 } 655 656 override fun handleUnreadNotifications(unreadNotifications: JSONArray) { 657 val pushNotificationHelper = PushNotificationHelper.getInstance() 658 pushNotificationHelper.removeNotifications(this, unreadNotifications) 659 } 660 661 /* 662 * 663 * Notification 664 * 665 */ 666 private fun addNotification() { 667 if (isShellApp || manifestUrl == null || manifest == null) { 668 return 669 } 670 671 val name = manifest!!.getName() ?: return 672 673 val remoteViews = RemoteViews(packageName, R.layout.notification) 674 remoteViews.setCharSequence(R.id.home_text_button, "setText", name) 675 676 // Home 677 val homeIntent = Intent(this, LauncherActivity::class.java) 678 remoteViews.setOnClickPendingIntent( 679 R.id.home_image_button, 680 PendingIntent.getActivity( 681 this, 0, 682 homeIntent, 0 683 ) 684 ) 685 686 // Reload 687 remoteViews.setOnClickPendingIntent( 688 R.id.reload_button, 689 PendingIntent.getService( 690 this, 0, 691 ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT 692 ) 693 ) 694 695 notificationRemoteViews = remoteViews 696 697 // Build the actual notification 698 val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 699 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 700 701 ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel() 702 notificationBuilder = 703 NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID) 704 .setContent(notificationRemoteViews) 705 .setSmallIcon(R.drawable.notification_icon) 706 .setShowWhen(false) 707 .setOngoing(true) 708 .setPriority(Notification.PRIORITY_MAX) 709 .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) 710 711 notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build()) 712 } 713 714 fun removeNotification() { 715 notificationRemoteViews = null 716 notificationBuilder = null 717 removeNotification(this) 718 } 719 720 fun onNotificationAction() { 721 dismissNuxViewIfVisible(true) 722 } 723 724 /** 725 * @param isFromNotification true if this is the result of the user taking an 726 * action in the notification view. 727 */ 728 fun dismissNuxViewIfVisible(isFromNotification: Boolean) { 729 if (nuxOverlayView == null) { 730 return 731 } 732 733 runOnUiThread { 734 val fadeOut: Animation = AlphaAnimation(1f, 0f).apply { 735 interpolator = AccelerateInterpolator() 736 duration = 500 737 setAnimationListener(object : Animation.AnimationListener { 738 override fun onAnimationEnd(animation: Animation) { 739 if (nuxOverlayView!!.parent != null) { 740 (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView) 741 } 742 nuxOverlayView = null 743 val eventProperties = JSONObject() 744 try { 745 eventProperties.put("IS_FROM_NOTIFICATION", isFromNotification) 746 } catch (e: JSONException) { 747 EXL.e(TAG, e.message) 748 } 749 Analytics.logEvent("NUX_EXPERIENCE_OVERLAY_DISMISSED", eventProperties) 750 } 751 752 override fun onAnimationRepeat(animation: Animation) {} 753 override fun onAnimationStart(animation: Animation) {} 754 }) 755 } 756 nuxOverlayView!!.startAnimation(fadeOut) 757 } 758 } 759 760 /* 761 * 762 * Errors 763 * 764 */ 765 override fun onError(intent: Intent) { 766 if (manifestUrl != null) { 767 intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl) 768 } 769 } 770 771 companion object { 772 private val TAG = ExperienceActivity::class.java.simpleName 773 private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" 774 const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 775 private const val READY_FOR_BUNDLE = "readyForBundle" 776 777 /** 778 * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user. 779 */ 780 var currentActivity: ExperienceActivity? = null 781 private set 782 783 @JvmStatic fun removeNotification(context: Context) { 784 val notificationManager = 785 context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 786 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 787 } 788 } 789 } 790