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