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 var managedAppSplashScreenViewController: ManagedAppSplashScreenViewController? = null 90 91 @Inject 92 lateinit var exponentManifest: ExponentManifest 93 94 @Inject 95 lateinit var devMenuManager: DevMenuManager 96 97 private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener = 98 object : DevBundleDownloadProgressListener { 99 override fun onProgress(status: String?, done: Int?, total: Int?) { 100 UiThreadUtil.runOnUiThread { 101 loadingProgressPopupController.updateProgress( 102 status, 103 done, 104 total 105 ) 106 } 107 } 108 109 override fun onSuccess() { 110 UiThreadUtil.runOnUiThread { 111 loadingProgressPopupController.hide() 112 managedAppSplashScreenViewController?.startSplashScreenWarningTimer() 113 finishLoading() 114 } 115 } 116 117 override fun onFailure(error: Exception) { 118 UiThreadUtil.runOnUiThread { 119 loadingProgressPopupController.hide() 120 interruptLoading() 121 } 122 } 123 } 124 125 /* 126 * 127 * Lifecycle 128 * 129 */ 130 override fun onCreate(savedInstanceState: Bundle?) { 131 super.onCreate(savedInstanceState) 132 133 isLoadExperienceAllowedToRun = true 134 shouldShowLoadingViewWithOptimisticManifest = true 135 loadingProgressPopupController = LoadingProgressPopupController(this) 136 137 NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this) 138 EventBus.getDefault().registerSticky(this) 139 140 activityId = ExpoActivityIds.getNextAppActivityId() 141 142 // TODO: audit this now that kernel logic is on the native side in Kotlin 143 var shouldOpenImmediately = true 144 145 // If our activity was killed for memory reasons or because of "Don't keep activities", 146 // try to reload manifest using the savedInstanceState 147 if (savedInstanceState != null) { 148 val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY) 149 if (manifestUrl != null) { 150 this.manifestUrl = manifestUrl 151 } 152 } 153 154 // On cold boot to experience, we're given this information from the Kotlin kernel, instead of 155 // the JS kernel. 156 val bundle = intent.extras 157 if (bundle != null && this.manifestUrl == null) { 158 val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY) 159 if (manifestUrl != null) { 160 this.manifestUrl = manifestUrl 161 } 162 163 // Don't want to get here if savedInstanceState has manifestUrl. Only care about 164 // IS_OPTIMISTIC_KEY the first time onCreate is called. 165 val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY) 166 if (isOptimistic) { 167 shouldOpenImmediately = false 168 } 169 } 170 171 if (this.manifestUrl != null && shouldOpenImmediately) { 172 val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false) 173 ExpoUpdatesAppLoader( 174 this.manifestUrl!!, 175 object : AppLoaderCallback { 176 override fun onOptimisticManifest(optimisticManifest: Manifest) { 177 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 178 } 179 180 override fun onManifestCompleted(manifest: Manifest) { 181 Exponent.instance.runOnUiThread { 182 try { 183 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 184 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl) 185 } catch (e: JSONException) { 186 kernel.handleError(e) 187 } 188 } 189 } 190 191 override fun onBundleCompleted(localBundlePath: String) { 192 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 193 } 194 195 override fun emitEvent(params: JSONObject) { 196 emitUpdatesEvent(params) 197 } 198 199 override fun updateStatus(status: AppLoaderStatus) { 200 setLoadingProgressStatusIfEnabled(status) 201 } 202 203 override fun onError(e: Exception) { 204 Exponent.instance.runOnUiThread { kernel.handleError(e) } 205 } 206 }, 207 forceCache 208 ).start(this) 209 } 210 kernel.setOptimisticActivity(this, taskId) 211 } 212 213 override fun onResume() { 214 super.onResume() 215 currentActivity = this 216 217 // Resume home's host if needed. 218 devMenuManager.maybeResumeHostWithActivity(this) 219 220 soLoaderInit() 221 222 addNotification() 223 Analytics.logEventWithManifestUrl(Analytics.AnalyticsEvent.EXPERIENCE_APPEARED, manifestUrl) 224 } 225 226 override fun onWindowFocusChanged(hasFocus: Boolean) { 227 super.onWindowFocusChanged(hasFocus) 228 // Check for manifest to avoid calling this when first loading an experience 229 if (hasFocus && manifest != null) { 230 runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) } 231 } 232 } 233 234 private fun soLoaderInit() { 235 if (detachSdkVersion != null) { 236 SoLoader.init(this, false) 237 } 238 } 239 240 open fun shouldCheckOptions() { 241 if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) { 242 handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!) 243 } 244 } 245 246 override fun onPause() { 247 super.onPause() 248 if (currentActivity === this) { 249 currentActivity = null 250 } 251 removeNotification() 252 Analytics.clearTimedEvents() 253 } 254 255 public override fun onSaveInstanceState(savedInstanceState: Bundle) { 256 savedInstanceState.putString(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 257 super.onSaveInstanceState(savedInstanceState) 258 } 259 260 override fun onNewIntent(intent: Intent) { 261 super.onNewIntent(intent) 262 val uri = intent.data 263 if (uri != null) { 264 handleUri(uri.toString()) 265 } 266 } 267 268 fun toggleDevMenu(): Boolean { 269 if (reactInstanceManager.isNotNull && !isCrashed) { 270 devMenuManager.toggleInActivity(this) 271 return true 272 } 273 return false 274 } 275 276 /** 277 * Handles command line command `adb shell input keyevent 82` that toggles the dev menu on the current experience activity. 278 */ 279 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 280 if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager.isNotNull && !isCrashed) { 281 devMenuManager.toggleInActivity(this) 282 return true 283 } 284 return super.onKeyUp(keyCode, event) 285 } 286 287 /** 288 * Closes the dev menu when pressing back button when it is visible on this activity. 289 */ 290 override fun onBackPressed() { 291 if (currentActivity === this && devMenuManager.isShownInActivity(this)) { 292 devMenuManager.requestToClose(this) 293 return 294 } 295 super.onBackPressed() 296 } 297 298 fun onEventMainThread(event: KernelStartedRunningEvent?) { 299 AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY) 300 } 301 302 override fun onDoneLoading() { 303 Analytics.markEvent(Analytics.TimedEvent.FINISHED_LOADING_REACT_NATIVE) 304 Analytics.sendTimedEvents(manifestUrl) 305 } 306 307 fun onEvent(event: ExperienceDoneLoadingEvent) { 308 if (event.activity === this) { 309 loadingProgressPopupController.hide() 310 } 311 312 if (!Constants.isStandaloneApp()) { 313 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 314 if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) { 315 AlertDialog.Builder(this@ExperienceActivity) 316 .setTitle("Using a cached project") 317 .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.") 318 .setPositiveButton("Use cache", null) 319 .setNegativeButton("Reload") { _, _ -> 320 kernel.reloadVisibleExperience( 321 manifestUrl!!, false 322 ) 323 } 324 .show() 325 } 326 } 327 } 328 329 /* 330 * 331 * Experience Loading 332 * 333 */ 334 fun startLoading() { 335 isLoading = true 336 showOrReconfigureManagedAppSplashScreen(manifest) 337 setLoadingProgressStatusIfEnabled() 338 } 339 340 /** 341 * This method is being called twice: 342 * - first time for optimistic manifest 343 * - seconds time for real manifest 344 */ 345 protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) { 346 if (!shouldCreateLoadingView()) { 347 return 348 } 349 350 hideLoadingView() 351 if (managedAppSplashScreenViewProvider == null) { 352 val config = ManagedAppSplashScreenConfiguration.parseManifest( 353 manifest!! 354 ) 355 managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config) 356 val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this) 357 managedAppSplashScreenViewController = ManagedAppSplashScreenViewController( 358 this, 359 getRootViewClass( 360 manifest 361 ), 362 splashScreenView 363 ) 364 SplashScreen.show(this, managedAppSplashScreenViewController!!, true) 365 } else { 366 managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!) 367 } 368 } 369 370 fun setLoadingProgressStatusIfEnabled() { 371 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 372 if (appLoader != null) { 373 setLoadingProgressStatusIfEnabled(appLoader.status) 374 } 375 } 376 377 fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) { 378 if (Constants.isStandaloneApp()) { 379 return 380 } 381 if (status == null) { 382 return 383 } 384 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 385 if (appLoader != null && appLoader.shouldShowAppLoaderStatus) { 386 UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) } 387 } else { 388 UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() } 389 } 390 } 391 392 fun setOptimisticManifest(optimisticManifest: Manifest) { 393 runOnUiThread { 394 if (!isInForeground) { 395 return@runOnUiThread 396 } 397 if (!shouldShowLoadingViewWithOptimisticManifest) { 398 return@runOnUiThread 399 } 400 ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity) 401 ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity) 402 ExperienceActivityUtils.setTaskDescription( 403 exponentManifest, 404 optimisticManifest, 405 this@ExperienceActivity 406 ) 407 showOrReconfigureManagedAppSplashScreen(optimisticManifest) 408 setLoadingProgressStatusIfEnabled() 409 } 410 } 411 412 fun setManifest( 413 manifestUrl: String, 414 manifest: Manifest, 415 bundleUrl: String 416 ) { 417 if (!isInForeground) { 418 return 419 } 420 if (!isLoadExperienceAllowedToRun) { 421 return 422 } 423 424 // Only want to run once per onCreate. There are some instances with ShellAppActivity where this would be called 425 // twice otherwise. Turn on "Don't keep activities", trigger a notification, background the app, and then 426 // press on the notification in a shell app to see this happen. 427 isLoadExperienceAllowedToRun = false 428 429 isReadyForBundle = false 430 this.manifestUrl = manifestUrl 431 this.manifest = manifest 432 433 exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!) 434 435 // Notifications logic uses this to determine which experience to route a notification to 436 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl)) 437 438 ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!) 439 440 val task = kernel.getExperienceActivityTask(this.manifestUrl!!) 441 task.taskId = taskId 442 task.experienceActivity = WeakReference(this) 443 task.activityId = activityId 444 task.bundleUrl = bundleUrl 445 446 sdkVersion = manifest.getSDKVersion() 447 isShellApp = this.manifestUrl == Constants.INITIAL_URL 448 449 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 450 // to point to the unversioned code in ReactAndroid. 451 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 452 sdkVersion = RNObject.UNVERSIONED 453 } 454 455 // In detach/shell, we always use UNVERSIONED as the ABI. 456 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 457 458 if (RNObject.UNVERSIONED != sdkVersion) { 459 var isValidVersion = false 460 for (version in Constants.SDK_VERSIONS_LIST) { 461 if (version == sdkVersion) { 462 isValidVersion = true 463 break 464 } 465 } 466 if (!isValidVersion) { 467 KernelProvider.instance.handleError( 468 sdkVersion + " is not a valid SDK version. Options are " + 469 TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "." 470 ) 471 return 472 } 473 } 474 475 soLoaderInit() 476 477 try { 478 experienceKey = ExperienceKey.fromManifest(manifest) 479 AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY) 480 } catch (e: JSONException) { 481 KernelProvider.instance.handleError("No ID found in manifest.") 482 return 483 } 484 485 isCrashed = false 486 487 Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion) 488 489 ExperienceActivityUtils.updateOrientation(this.manifest!!, this) 490 ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest!!, this) 491 ExperienceActivityUtils.overrideUiMode(this.manifest!!, this) 492 493 addNotification() 494 495 var notificationObject: ExponentNotification? = null 496 // Activity could be restarted due to Dark Mode change, only pop options if that will not happen 497 if (kernel.hasOptionsForManifestUrl(manifestUrl)) { 498 val options = kernel.popOptionsForManifestUrl(manifestUrl) 499 500 // if the kernel has an intent for our manifest url, that's the intent that triggered 501 // the loading of this experience. 502 if (options!!.uri != null) { 503 intentUri = options.uri 504 } 505 notificationObject = options.notificationObject 506 } 507 508 BranchManager.handleLink(this, intentUri, detachSdkVersion) 509 510 runOnUiThread { 511 if (!isInForeground) { 512 return@runOnUiThread 513 } 514 if (reactInstanceManager.isNotNull) { 515 reactInstanceManager.onHostDestroy() 516 reactInstanceManager.assign(null) 517 } 518 519 reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView") 520 reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity) 521 setReactRootView((reactRootView.get() as View)) 522 523 if (isDebugModeEnabled) { 524 notification = notificationObject 525 jsBundlePath = "" 526 startReactInstance() 527 } else { 528 tempNotification = notificationObject 529 isReadyForBundle = true 530 AsyncCondition.notify(READY_FOR_BUNDLE) 531 } 532 533 ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity) 534 ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity) 535 ExperienceActivityUtils.setTaskDescription( 536 exponentManifest, 537 manifest, 538 this@ExperienceActivity 539 ) 540 showOrReconfigureManagedAppSplashScreen(manifest) 541 setLoadingProgressStatusIfEnabled() 542 } 543 } 544 545 fun setBundle(localBundlePath: String) { 546 // by this point, setManifest should have also been called, so prevent 547 // setOptimisticManifest from showing a rogue splash screen 548 shouldShowLoadingViewWithOptimisticManifest = false 549 if (!isDebugModeEnabled) { 550 val finalIsReadyForBundle = isReadyForBundle 551 AsyncCondition.wait( 552 READY_FOR_BUNDLE, 553 object : AsyncConditionListener { 554 override fun isReady(): Boolean { 555 return finalIsReadyForBundle 556 } 557 558 override fun execute() { 559 notification = tempNotification 560 tempNotification = null 561 jsBundlePath = localBundlePath 562 startReactInstance() 563 AsyncCondition.remove(READY_FOR_BUNDLE) 564 } 565 } 566 ) 567 } 568 } 569 570 fun onEventMainThread(event: ReceivedNotificationEvent) { 571 // TODO(wschurman): investigate removal, this probably is no longer used 572 if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) { 573 try { 574 val rctDeviceEventEmitter = 575 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 576 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 577 reactInstanceManager.callRecursive("getCurrentReactContext")!! 578 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 579 .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received")) 580 } catch (e: Throwable) { 581 EXL.e(TAG, e) 582 } 583 } 584 } 585 586 fun handleOptions(options: ExperienceOptions) { 587 try { 588 val uri = options.uri 589 if (uri !== null) { 590 handleUri(uri) 591 val rctDeviceEventEmitter = 592 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 593 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 594 reactInstanceManager.callRecursive("getCurrentReactContext")!! 595 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 596 .call("emit", "Exponent.openUri", uri) 597 BranchManager.handleLink(this, uri, detachSdkVersion) 598 } 599 if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) { 600 val rctDeviceEventEmitter = 601 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 602 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 603 reactInstanceManager.callRecursive("getCurrentReactContext")!! 604 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 605 .call( 606 "emit", 607 "Exponent.notification", 608 options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected") 609 ) 610 } 611 } catch (e: Throwable) { 612 EXL.e(TAG, e) 613 } 614 } 615 616 private fun handleUri(uri: String) { 617 // Emits a "url" event to the Linking event emitter 618 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) 619 super.onNewIntent(intent) 620 } 621 622 fun emitUpdatesEvent(params: JSONObject) { 623 KernelProvider.instance.addEventForExperience( 624 manifestUrl!!, 625 KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString()) 626 ) 627 } 628 629 override val isDebugModeEnabled: Boolean 630 get() = manifest?.isDevelopmentMode() ?: false 631 632 override fun startReactInstance() { 633 Exponent.instance 634 .testPackagerStatus( 635 isDebugModeEnabled, manifest!!, 636 object : Exponent.PackagerStatusCallback { 637 override fun onSuccess() { 638 reactInstanceManager = startReactInstance( 639 this@ExperienceActivity, 640 intentUri, 641 detachSdkVersion, 642 notification, 643 isShellApp, 644 reactPackages(), 645 expoPackages(), 646 devBundleDownloadProgressListener 647 ) 648 } 649 650 override fun onFailure(errorMessage: String) { 651 KernelProvider.instance.handleError(errorMessage) 652 } 653 } 654 ) 655 } 656 657 override fun handleUnreadNotifications(unreadNotifications: JSONArray) { 658 PushNotificationHelper.instance.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(Analytics.AnalyticsEvent.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