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 com.google.firebase.crashlytics.FirebaseCrashlytics 27 import de.greenrobot.event.EventBus 28 import expo.modules.core.interfaces.Package 29 import expo.modules.manifests.core.Manifest 30 import expo.modules.splashscreen.singletons.SplashScreen 31 import host.exp.exponent.* 32 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 33 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 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.ExperienceRTLManager 51 import host.exp.exponent.utils.ExpoActivityIds 52 import host.exp.expoview.Exponent 53 import host.exp.expoview.Exponent.StartReactInstanceDelegate 54 import host.exp.expoview.R 55 import org.json.JSONArray 56 import org.json.JSONException 57 import org.json.JSONObject 58 import versioned.host.exp.exponent.ExponentPackageDelegate 59 import versioned.host.exp.exponent.ReactUnthemedRootView 60 import java.lang.ref.WeakReference 61 import javax.inject.Inject 62 63 open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDelegate { 64 open fun expoPackages(): List<Package>? { 65 // Experience must pick its own modules in ExponentPackage 66 return null 67 } 68 69 open fun reactPackages(): List<ReactPackage>? { 70 return null 71 } 72 73 override val exponentPackageDelegate: ExponentPackageDelegate? = null 74 75 private var nuxOverlayView: ReactUnthemedRootView? = null 76 private var notification: ExponentNotification? = null 77 private var tempNotification: ExponentNotification? = null 78 private var isShellApp = false 79 protected var intentUri: String? = null 80 private var isReadyForBundle = false 81 private var notificationRemoteViews: RemoteViews? = null 82 private var notificationBuilder: NotificationCompat.Builder? = null 83 private var isLoadExperienceAllowedToRun = false 84 private var shouldShowLoadingViewWithOptimisticManifest = false 85 86 /** 87 * Controls loadingProgressPopupWindow that is shown above whole activity. 88 */ 89 lateinit var loadingProgressPopupController: LoadingProgressPopupController 90 var managedAppSplashScreenViewProvider: ManagedAppSplashScreenViewProvider? = null 91 var managedAppSplashScreenViewController: ManagedAppSplashScreenViewController? = null 92 93 @Inject 94 lateinit var exponentManifest: ExponentManifest 95 96 @Inject 97 lateinit var devMenuManager: DevMenuManager 98 99 private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener = 100 object : DevBundleDownloadProgressListener { 101 override fun onProgress(status: String?, done: Int?, total: Int?) { 102 UiThreadUtil.runOnUiThread { 103 loadingProgressPopupController.updateProgress( 104 status, 105 done, 106 total 107 ) 108 } 109 } 110 111 override fun onSuccess() { 112 UiThreadUtil.runOnUiThread { 113 loadingProgressPopupController.hide() 114 managedAppSplashScreenViewController?.startSplashScreenWarningTimer() 115 finishLoading() 116 } 117 } 118 119 override fun onFailure(error: Exception) { 120 UiThreadUtil.runOnUiThread { 121 loadingProgressPopupController.hide() 122 interruptLoading() 123 } 124 } 125 } 126 127 /* 128 * 129 * Lifecycle 130 * 131 */ 132 override fun onCreate(savedInstanceState: Bundle?) { 133 super.onCreate(savedInstanceState) 134 135 isLoadExperienceAllowedToRun = true 136 shouldShowLoadingViewWithOptimisticManifest = true 137 loadingProgressPopupController = LoadingProgressPopupController(this) 138 139 NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this) 140 EventBus.getDefault().registerSticky(this) 141 142 activityId = ExpoActivityIds.getNextAppActivityId() 143 144 // TODO: audit this now that kernel logic is on the native side in Kotlin 145 var shouldOpenImmediately = true 146 147 // If our activity was killed for memory reasons or because of "Don't keep activities", 148 // try to reload manifest using the savedInstanceState 149 if (savedInstanceState != null) { 150 val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY) 151 if (manifestUrl != null) { 152 this.manifestUrl = manifestUrl 153 } 154 } 155 156 // On cold boot to experience, we're given this information from the Kotlin kernel, instead of 157 // the JS kernel. 158 val bundle = intent.extras 159 if (bundle != null && this.manifestUrl == null) { 160 val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY) 161 if (manifestUrl != null) { 162 this.manifestUrl = manifestUrl 163 } 164 165 // Don't want to get here if savedInstanceState has manifestUrl. Only care about 166 // IS_OPTIMISTIC_KEY the first time onCreate is called. 167 val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY) 168 if (isOptimistic) { 169 shouldOpenImmediately = false 170 } 171 } 172 173 FirebaseCrashlytics.getInstance().log("ExperienceActivity.manifestUrl: ${this.manifestUrl}") 174 if (this.manifestUrl != null && shouldOpenImmediately) { 175 val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false) 176 ExpoUpdatesAppLoader( 177 this.manifestUrl!!, 178 object : AppLoaderCallback { 179 override fun onOptimisticManifest(optimisticManifest: Manifest) { 180 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 181 } 182 183 override fun onManifestCompleted(manifest: Manifest) { 184 Exponent.instance.runOnUiThread { 185 try { 186 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 187 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl) 188 } catch (e: JSONException) { 189 kernel.handleError(e) 190 } 191 } 192 } 193 194 override fun onBundleCompleted(localBundlePath: String) { 195 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 196 } 197 198 override fun emitEvent(params: JSONObject) { 199 emitUpdatesEvent(params) 200 } 201 202 override fun updateStatus(status: AppLoaderStatus) { 203 setLoadingProgressStatusIfEnabled(status) 204 } 205 206 override fun onError(e: Exception) { 207 Exponent.instance.runOnUiThread { kernel.handleError(e) } 208 } 209 }, 210 forceCache 211 ).start(this) 212 } 213 kernel.setOptimisticActivity(this, taskId) 214 } 215 216 override fun onResume() { 217 super.onResume() 218 currentActivity = this 219 220 // Resume home's host if needed. 221 devMenuManager.maybeResumeHostWithActivity(this) 222 223 soLoaderInit() 224 225 addNotification() 226 } 227 228 override fun onWindowFocusChanged(hasFocus: Boolean) { 229 super.onWindowFocusChanged(hasFocus) 230 // Check for manifest to avoid calling this when first loading an experience 231 if (hasFocus && manifest != null) { 232 runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) } 233 } 234 } 235 236 private fun soLoaderInit() { 237 if (detachSdkVersion != null) { 238 SoLoader.init(this, false) 239 } 240 } 241 242 open fun shouldCheckOptions() { 243 if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) { 244 handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!) 245 } 246 } 247 248 override fun onPause() { 249 super.onPause() 250 if (currentActivity === this) { 251 currentActivity = null 252 } 253 removeNotification() 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 } 305 306 fun onEvent(event: ExperienceDoneLoadingEvent) { 307 if (event.activity === this) { 308 loadingProgressPopupController.hide() 309 } 310 311 if (!Constants.isStandaloneApp()) { 312 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 313 if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) { 314 AlertDialog.Builder(this@ExperienceActivity) 315 .setTitle("Using a cached project") 316 .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.") 317 .setPositiveButton("Use cache", null) 318 .setNegativeButton("Reload") { _, _ -> 319 kernel.reloadVisibleExperience( 320 manifestUrl!!, false 321 ) 322 } 323 .show() 324 } 325 } 326 } 327 328 /* 329 * 330 * Experience Loading 331 * 332 */ 333 fun startLoading() { 334 isLoading = true 335 showOrReconfigureManagedAppSplashScreen(manifest) 336 setLoadingProgressStatusIfEnabled() 337 } 338 339 /** 340 * This method is being called twice: 341 * - first time for optimistic manifest 342 * - seconds time for real manifest 343 */ 344 protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) { 345 if (!shouldCreateLoadingView()) { 346 return 347 } 348 349 hideLoadingView() 350 if (managedAppSplashScreenViewProvider == null) { 351 val config = ManagedAppSplashScreenConfiguration.parseManifest( 352 manifest!! 353 ) 354 managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config) 355 val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this) 356 managedAppSplashScreenViewController = ManagedAppSplashScreenViewController( 357 this, 358 getRootViewClass( 359 manifest 360 ), 361 splashScreenView 362 ) 363 SplashScreen.show(this, managedAppSplashScreenViewController!!, true) 364 } else { 365 managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!) 366 } 367 } 368 369 fun setLoadingProgressStatusIfEnabled() { 370 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 371 if (appLoader != null) { 372 setLoadingProgressStatusIfEnabled(appLoader.status) 373 } 374 } 375 376 fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) { 377 if (Constants.isStandaloneApp()) { 378 return 379 } 380 if (status == null) { 381 return 382 } 383 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 384 if (appLoader != null && appLoader.shouldShowAppLoaderStatus) { 385 UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) } 386 } else { 387 UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() } 388 } 389 } 390 391 fun setOptimisticManifest(optimisticManifest: Manifest) { 392 runOnUiThread { 393 if (!isInForeground) { 394 return@runOnUiThread 395 } 396 if (!shouldShowLoadingViewWithOptimisticManifest) { 397 return@runOnUiThread 398 } 399 ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity) 400 ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity) 401 ExperienceActivityUtils.setTaskDescription( 402 exponentManifest, 403 optimisticManifest, 404 this@ExperienceActivity 405 ) 406 showOrReconfigureManagedAppSplashScreen(optimisticManifest) 407 setLoadingProgressStatusIfEnabled() 408 ExperienceRTLManager.setSupportsRTLFromManifest(this, optimisticManifest) 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.getExpoGoSDKVersion() 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 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 ExperienceRTLManager.setSupportsRTLFromManifest(this, manifest) 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 // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability 677 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 678 679 // Home 680 val homeIntent = Intent(this, LauncherActivity::class.java) 681 remoteViews.setOnClickPendingIntent( 682 R.id.home_image_button, 683 PendingIntent.getActivity( 684 this, 0, 685 homeIntent, mutableFlag 686 ) 687 ) 688 689 // Reload 690 remoteViews.setOnClickPendingIntent( 691 R.id.reload_button, 692 PendingIntent.getService( 693 this, 0, 694 ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag 695 ) 696 ) 697 698 notificationRemoteViews = remoteViews 699 700 // Build the actual notification 701 val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 702 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 703 704 ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel() 705 notificationBuilder = 706 NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID) 707 .setContent(notificationRemoteViews) 708 .setSmallIcon(R.drawable.notification_icon) 709 .setShowWhen(false) 710 .setOngoing(true) 711 .setPriority(Notification.PRIORITY_MAX) 712 .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) 713 714 notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build()) 715 } 716 717 fun removeNotification() { 718 notificationRemoteViews = null 719 notificationBuilder = null 720 removeNotification(this) 721 } 722 723 fun onNotificationAction() { 724 dismissNuxViewIfVisible(true) 725 } 726 727 /** 728 * @param isFromNotification true if this is the result of the user taking an 729 * action in the notification view. 730 */ 731 fun dismissNuxViewIfVisible(isFromNotification: Boolean) { 732 if (nuxOverlayView == null) { 733 return 734 } 735 736 runOnUiThread { 737 val fadeOut: Animation = AlphaAnimation(1f, 0f).apply { 738 interpolator = AccelerateInterpolator() 739 duration = 500 740 setAnimationListener(object : Animation.AnimationListener { 741 override fun onAnimationEnd(animation: Animation) { 742 if (nuxOverlayView!!.parent != null) { 743 (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView) 744 } 745 nuxOverlayView = null 746 } 747 748 override fun onAnimationRepeat(animation: Animation) {} 749 override fun onAnimationStart(animation: Animation) {} 750 }) 751 } 752 nuxOverlayView!!.startAnimation(fadeOut) 753 } 754 } 755 756 /* 757 * 758 * Errors 759 * 760 */ 761 override fun onError(intent: Intent) { 762 if (manifestUrl != null) { 763 intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl) 764 } 765 } 766 767 companion object { 768 private val TAG = ExperienceActivity::class.java.simpleName 769 private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" 770 const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 771 private const val READY_FOR_BUNDLE = "readyForBundle" 772 773 /** 774 * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user. 775 */ 776 var currentActivity: ExperienceActivity? = null 777 private set 778 779 @JvmStatic fun removeNotification(context: Context) { 780 val notificationManager = 781 context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 782 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 783 } 784 } 785 } 786