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