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.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 if (this.manifestUrl != null && shouldOpenImmediately) { 174 val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false) 175 ExpoUpdatesAppLoader( 176 this.manifestUrl!!, 177 object : AppLoaderCallback { 178 override fun onOptimisticManifest(optimisticManifest: Manifest) { 179 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 180 } 181 182 override fun onManifestCompleted(manifest: Manifest) { 183 Exponent.instance.runOnUiThread { 184 try { 185 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 186 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl) 187 } catch (e: JSONException) { 188 kernel.handleError(e) 189 } 190 } 191 } 192 193 override fun onBundleCompleted(localBundlePath: String) { 194 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 195 } 196 197 override fun emitEvent(params: JSONObject) { 198 emitUpdatesEvent(params) 199 } 200 201 override fun updateStatus(status: AppLoaderStatus) { 202 setLoadingProgressStatusIfEnabled(status) 203 } 204 205 override fun onError(e: Exception) { 206 Exponent.instance.runOnUiThread { kernel.handleError(e) } 207 } 208 }, 209 forceCache 210 ).start(this) 211 } 212 kernel.setOptimisticActivity(this, taskId) 213 } 214 215 override fun onResume() { 216 super.onResume() 217 currentActivity = this 218 219 // Resume home's host if needed. 220 devMenuManager.maybeResumeHostWithActivity(this) 221 222 soLoaderInit() 223 224 addNotification() 225 Analytics.logEventWithManifestUrl(Analytics.AnalyticsEvent.EXPERIENCE_APPEARED, manifestUrl) 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 Analytics.clearTimedEvents() 255 } 256 257 public override fun onSaveInstanceState(savedInstanceState: Bundle) { 258 savedInstanceState.putString(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 259 super.onSaveInstanceState(savedInstanceState) 260 } 261 262 override fun onNewIntent(intent: Intent) { 263 super.onNewIntent(intent) 264 val uri = intent.data 265 if (uri != null) { 266 handleUri(uri.toString()) 267 } 268 } 269 270 fun toggleDevMenu(): Boolean { 271 if (reactInstanceManager.isNotNull && !isCrashed) { 272 devMenuManager.toggleInActivity(this) 273 return true 274 } 275 return false 276 } 277 278 /** 279 * Handles command line command `adb shell input keyevent 82` that toggles the dev menu on the current experience activity. 280 */ 281 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 282 if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager.isNotNull && !isCrashed) { 283 devMenuManager.toggleInActivity(this) 284 return true 285 } 286 return super.onKeyUp(keyCode, event) 287 } 288 289 /** 290 * Closes the dev menu when pressing back button when it is visible on this activity. 291 */ 292 override fun onBackPressed() { 293 if (currentActivity === this && devMenuManager.isShownInActivity(this)) { 294 devMenuManager.requestToClose(this) 295 return 296 } 297 super.onBackPressed() 298 } 299 300 fun onEventMainThread(event: KernelStartedRunningEvent?) { 301 AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY) 302 } 303 304 override fun onDoneLoading() { 305 Analytics.markEvent(Analytics.TimedEvent.FINISHED_LOADING_REACT_NATIVE) 306 Analytics.sendTimedEvents(manifestUrl) 307 } 308 309 fun onEvent(event: ExperienceDoneLoadingEvent) { 310 if (event.activity === this) { 311 loadingProgressPopupController.hide() 312 } 313 314 if (!Constants.isStandaloneApp()) { 315 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 316 if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) { 317 AlertDialog.Builder(this@ExperienceActivity) 318 .setTitle("Using a cached project") 319 .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.") 320 .setPositiveButton("Use cache", null) 321 .setNegativeButton("Reload") { _, _ -> 322 kernel.reloadVisibleExperience( 323 manifestUrl!!, false 324 ) 325 } 326 .show() 327 } 328 } 329 } 330 331 /* 332 * 333 * Experience Loading 334 * 335 */ 336 fun startLoading() { 337 isLoading = true 338 showOrReconfigureManagedAppSplashScreen(manifest) 339 setLoadingProgressStatusIfEnabled() 340 } 341 342 /** 343 * This method is being called twice: 344 * - first time for optimistic manifest 345 * - seconds time for real manifest 346 */ 347 protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) { 348 if (!shouldCreateLoadingView()) { 349 return 350 } 351 352 hideLoadingView() 353 if (managedAppSplashScreenViewProvider == null) { 354 val config = ManagedAppSplashScreenConfiguration.parseManifest( 355 manifest!! 356 ) 357 managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config) 358 val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this) 359 managedAppSplashScreenViewController = ManagedAppSplashScreenViewController( 360 this, 361 getRootViewClass( 362 manifest 363 ), 364 splashScreenView 365 ) 366 SplashScreen.show(this, managedAppSplashScreenViewController!!, true) 367 } else { 368 managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!) 369 } 370 } 371 372 fun setLoadingProgressStatusIfEnabled() { 373 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 374 if (appLoader != null) { 375 setLoadingProgressStatusIfEnabled(appLoader.status) 376 } 377 } 378 379 fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) { 380 if (Constants.isStandaloneApp()) { 381 return 382 } 383 if (status == null) { 384 return 385 } 386 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 387 if (appLoader != null && appLoader.shouldShowAppLoaderStatus) { 388 UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) } 389 } else { 390 UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() } 391 } 392 } 393 394 fun setOptimisticManifest(optimisticManifest: Manifest) { 395 runOnUiThread { 396 if (!isInForeground) { 397 return@runOnUiThread 398 } 399 if (!shouldShowLoadingViewWithOptimisticManifest) { 400 return@runOnUiThread 401 } 402 ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity) 403 ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity) 404 ExperienceActivityUtils.setTaskDescription( 405 exponentManifest, 406 optimisticManifest, 407 this@ExperienceActivity 408 ) 409 showOrReconfigureManagedAppSplashScreen(optimisticManifest) 410 setLoadingProgressStatusIfEnabled() 411 ExperienceRTLManager.setSupportsRTLFromManifest(this, optimisticManifest) 412 } 413 } 414 415 fun setManifest( 416 manifestUrl: String, 417 manifest: Manifest, 418 bundleUrl: String 419 ) { 420 if (!isInForeground) { 421 return 422 } 423 if (!isLoadExperienceAllowedToRun) { 424 return 425 } 426 427 // Only want to run once per onCreate. There are some instances with ShellAppActivity where this would be called 428 // twice otherwise. Turn on "Don't keep activities", trigger a notification, background the app, and then 429 // press on the notification in a shell app to see this happen. 430 isLoadExperienceAllowedToRun = false 431 432 isReadyForBundle = false 433 this.manifestUrl = manifestUrl 434 this.manifest = manifest 435 436 exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!) 437 438 // Notifications logic uses this to determine which experience to route a notification to 439 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl)) 440 441 ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!) 442 443 val task = kernel.getExperienceActivityTask(this.manifestUrl!!) 444 task.taskId = taskId 445 task.experienceActivity = WeakReference(this) 446 task.activityId = activityId 447 task.bundleUrl = bundleUrl 448 449 sdkVersion = manifest.getSDKVersion() 450 isShellApp = this.manifestUrl == Constants.INITIAL_URL 451 452 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 453 // to point to the unversioned code in ReactAndroid. 454 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 455 sdkVersion = RNObject.UNVERSIONED 456 } 457 458 // In detach/shell, we always use UNVERSIONED as the ABI. 459 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 460 461 if (RNObject.UNVERSIONED != sdkVersion) { 462 var isValidVersion = false 463 for (version in Constants.SDK_VERSIONS_LIST) { 464 if (version == sdkVersion) { 465 isValidVersion = true 466 break 467 } 468 } 469 if (!isValidVersion) { 470 KernelProvider.instance.handleError( 471 sdkVersion + " is not a valid SDK version. Options are " + 472 TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "." 473 ) 474 return 475 } 476 } 477 478 soLoaderInit() 479 480 try { 481 experienceKey = ExperienceKey.fromManifest(manifest) 482 AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY) 483 } catch (e: JSONException) { 484 KernelProvider.instance.handleError("No ID found in manifest.") 485 return 486 } 487 488 isCrashed = false 489 490 Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion) 491 492 ExperienceActivityUtils.updateOrientation(this.manifest!!, this) 493 ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest!!, this) 494 ExperienceActivityUtils.overrideUiMode(this.manifest!!, this) 495 496 addNotification() 497 498 var notificationObject: ExponentNotification? = null 499 // Activity could be restarted due to Dark Mode change, only pop options if that will not happen 500 if (kernel.hasOptionsForManifestUrl(manifestUrl)) { 501 val options = kernel.popOptionsForManifestUrl(manifestUrl) 502 503 // if the kernel has an intent for our manifest url, that's the intent that triggered 504 // the loading of this experience. 505 if (options!!.uri != null) { 506 intentUri = options.uri 507 } 508 notificationObject = options.notificationObject 509 } 510 511 BranchManager.handleLink(this, intentUri, detachSdkVersion) 512 513 ExperienceRTLManager.setSupportsRTLFromManifest(this, manifest) 514 515 runOnUiThread { 516 if (!isInForeground) { 517 return@runOnUiThread 518 } 519 if (reactInstanceManager.isNotNull) { 520 reactInstanceManager.onHostDestroy() 521 reactInstanceManager.assign(null) 522 } 523 524 reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView") 525 reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity) 526 setReactRootView((reactRootView.get() as View)) 527 528 if (isDebugModeEnabled) { 529 notification = notificationObject 530 jsBundlePath = "" 531 startReactInstance() 532 } else { 533 tempNotification = notificationObject 534 isReadyForBundle = true 535 AsyncCondition.notify(READY_FOR_BUNDLE) 536 } 537 538 ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity) 539 ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity) 540 ExperienceActivityUtils.setTaskDescription( 541 exponentManifest, 542 manifest, 543 this@ExperienceActivity 544 ) 545 showOrReconfigureManagedAppSplashScreen(manifest) 546 setLoadingProgressStatusIfEnabled() 547 } 548 } 549 550 fun setBundle(localBundlePath: String) { 551 // by this point, setManifest should have also been called, so prevent 552 // setOptimisticManifest from showing a rogue splash screen 553 shouldShowLoadingViewWithOptimisticManifest = false 554 if (!isDebugModeEnabled) { 555 val finalIsReadyForBundle = isReadyForBundle 556 AsyncCondition.wait( 557 READY_FOR_BUNDLE, 558 object : AsyncConditionListener { 559 override fun isReady(): Boolean { 560 return finalIsReadyForBundle 561 } 562 563 override fun execute() { 564 notification = tempNotification 565 tempNotification = null 566 jsBundlePath = localBundlePath 567 startReactInstance() 568 AsyncCondition.remove(READY_FOR_BUNDLE) 569 } 570 } 571 ) 572 } 573 } 574 575 fun onEventMainThread(event: ReceivedNotificationEvent) { 576 // TODO(wschurman): investigate removal, this probably is no longer used 577 if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) { 578 try { 579 val rctDeviceEventEmitter = 580 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 581 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 582 reactInstanceManager.callRecursive("getCurrentReactContext")!! 583 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 584 .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received")) 585 } catch (e: Throwable) { 586 EXL.e(TAG, e) 587 } 588 } 589 } 590 591 fun handleOptions(options: ExperienceOptions) { 592 try { 593 val uri = options.uri 594 if (uri !== null) { 595 handleUri(uri) 596 val rctDeviceEventEmitter = 597 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 598 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 599 reactInstanceManager.callRecursive("getCurrentReactContext")!! 600 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 601 .call("emit", "Exponent.openUri", uri) 602 BranchManager.handleLink(this, uri, detachSdkVersion) 603 } 604 if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) { 605 val rctDeviceEventEmitter = 606 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 607 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 608 reactInstanceManager.callRecursive("getCurrentReactContext")!! 609 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 610 .call( 611 "emit", 612 "Exponent.notification", 613 options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected") 614 ) 615 } 616 } catch (e: Throwable) { 617 EXL.e(TAG, e) 618 } 619 } 620 621 private fun handleUri(uri: String) { 622 // Emits a "url" event to the Linking event emitter 623 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) 624 super.onNewIntent(intent) 625 } 626 627 fun emitUpdatesEvent(params: JSONObject) { 628 KernelProvider.instance.addEventForExperience( 629 manifestUrl!!, 630 KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString()) 631 ) 632 } 633 634 override val isDebugModeEnabled: Boolean 635 get() = manifest?.isDevelopmentMode() ?: false 636 637 override fun startReactInstance() { 638 Exponent.instance 639 .testPackagerStatus( 640 isDebugModeEnabled, manifest!!, 641 object : Exponent.PackagerStatusCallback { 642 override fun onSuccess() { 643 reactInstanceManager = startReactInstance( 644 this@ExperienceActivity, 645 intentUri, 646 detachSdkVersion, 647 notification, 648 isShellApp, 649 reactPackages(), 650 expoPackages(), 651 devBundleDownloadProgressListener 652 ) 653 } 654 655 override fun onFailure(errorMessage: String) { 656 KernelProvider.instance.handleError(errorMessage) 657 } 658 } 659 ) 660 } 661 662 override fun handleUnreadNotifications(unreadNotifications: JSONArray) { 663 PushNotificationHelper.instance.removeNotifications(this, unreadNotifications) 664 } 665 666 /* 667 * 668 * Notification 669 * 670 */ 671 private fun addNotification() { 672 if (isShellApp || manifestUrl == null || manifest == null) { 673 return 674 } 675 676 val name = manifest!!.getName() ?: return 677 678 val remoteViews = RemoteViews(packageName, R.layout.notification) 679 remoteViews.setCharSequence(R.id.home_text_button, "setText", name) 680 681 // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability 682 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 683 684 // Home 685 val homeIntent = Intent(this, LauncherActivity::class.java) 686 remoteViews.setOnClickPendingIntent( 687 R.id.home_image_button, 688 PendingIntent.getActivity( 689 this, 0, 690 homeIntent, mutableFlag 691 ) 692 ) 693 694 // Reload 695 remoteViews.setOnClickPendingIntent( 696 R.id.reload_button, 697 PendingIntent.getService( 698 this, 0, 699 ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag 700 ) 701 ) 702 703 notificationRemoteViews = remoteViews 704 705 // Build the actual notification 706 val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 707 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 708 709 ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel() 710 notificationBuilder = 711 NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID) 712 .setContent(notificationRemoteViews) 713 .setSmallIcon(R.drawable.notification_icon) 714 .setShowWhen(false) 715 .setOngoing(true) 716 .setPriority(Notification.PRIORITY_MAX) 717 .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) 718 719 notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build()) 720 } 721 722 fun removeNotification() { 723 notificationRemoteViews = null 724 notificationBuilder = null 725 removeNotification(this) 726 } 727 728 fun onNotificationAction() { 729 dismissNuxViewIfVisible(true) 730 } 731 732 /** 733 * @param isFromNotification true if this is the result of the user taking an 734 * action in the notification view. 735 */ 736 fun dismissNuxViewIfVisible(isFromNotification: Boolean) { 737 if (nuxOverlayView == null) { 738 return 739 } 740 741 runOnUiThread { 742 val fadeOut: Animation = AlphaAnimation(1f, 0f).apply { 743 interpolator = AccelerateInterpolator() 744 duration = 500 745 setAnimationListener(object : Animation.AnimationListener { 746 override fun onAnimationEnd(animation: Animation) { 747 if (nuxOverlayView!!.parent != null) { 748 (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView) 749 } 750 nuxOverlayView = null 751 val eventProperties = JSONObject() 752 try { 753 eventProperties.put("IS_FROM_NOTIFICATION", isFromNotification) 754 } catch (e: JSONException) { 755 EXL.e(TAG, e.message) 756 } 757 Analytics.logEvent(Analytics.AnalyticsEvent.NUX_EXPERIENCE_OVERLAY_DISMISSED, eventProperties) 758 } 759 760 override fun onAnimationRepeat(animation: Animation) {} 761 override fun onAnimationStart(animation: Animation) {} 762 }) 763 } 764 nuxOverlayView!!.startAnimation(fadeOut) 765 } 766 } 767 768 /* 769 * 770 * Errors 771 * 772 */ 773 override fun onError(intent: Intent) { 774 if (manifestUrl != null) { 775 intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl) 776 } 777 } 778 779 companion object { 780 private val TAG = ExperienceActivity::class.java.simpleName 781 private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" 782 const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 783 private const val READY_FOR_BUNDLE = "readyForBundle" 784 785 /** 786 * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user. 787 */ 788 var currentActivity: ExperienceActivity? = null 789 private set 790 791 @JvmStatic fun removeNotification(context: Context) { 792 val notificationManager = 793 context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 794 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 795 } 796 } 797 } 798