1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.experience
3 
4 import android.app.AlertDialog
5 import android.app.Notification
6 import android.app.NotificationManager
7 import android.app.PendingIntent
8 import android.content.Context
9 import android.content.Intent
10 import android.net.Uri
11 import android.os.Bundle
12 import android.text.TextUtils
13 import android.view.KeyEvent
14 import android.view.View
15 import android.view.ViewGroup
16 import android.view.animation.AccelerateInterpolator
17 import android.view.animation.AlphaAnimation
18 import android.view.animation.Animation
19 import android.widget.RemoteViews
20 import androidx.core.app.NotificationCompat
21 import androidx.core.content.ContextCompat
22 import com.facebook.react.ReactPackage
23 import com.facebook.react.bridge.UiThreadUtil
24 import com.facebook.soloader.SoLoader
25 import de.greenrobot.event.EventBus
26 import expo.modules.core.interfaces.Package
27 import expo.modules.splashscreen.singletons.SplashScreen
28 import expo.modules.manifests.core.Manifest
29 import host.exp.exponent.*
30 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback
31 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus
32 import host.exp.exponent.analytics.Analytics
33 import host.exp.exponent.analytics.EXL
34 import host.exp.exponent.branch.BranchManager
35 import host.exp.exponent.di.NativeModuleDepsProvider
36 import host.exp.exponent.experience.loading.LoadingProgressPopupController
37 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenConfiguration
38 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewController
39 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewProvider
40 import host.exp.exponent.kernel.*
41 import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent
42 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions
43 import host.exp.exponent.notifications.*
44 import host.exp.exponent.storage.ExponentDB
45 import host.exp.exponent.storage.ExponentDBObject
46 import host.exp.exponent.utils.AsyncCondition
47 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
48 import host.exp.exponent.utils.ExperienceActivityUtils
49 import host.exp.exponent.utils.ExpoActivityIds
50 import host.exp.expoview.Exponent
51 import host.exp.expoview.Exponent.StartReactInstanceDelegate
52 import host.exp.expoview.R
53 import org.json.JSONArray
54 import org.json.JSONException
55 import org.json.JSONObject
56 import versioned.host.exp.exponent.ExponentPackageDelegate
57 import versioned.host.exp.exponent.ReactUnthemedRootView
58 import java.lang.ref.WeakReference
59 import javax.inject.Inject
60 
61 open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDelegate {
62   open fun expoPackages(): List<Package>? {
63     // Experience must pick its own modules in ExponentPackage
64     return null
65   }
66 
67   open fun reactPackages(): List<ReactPackage>? {
68     return null
69   }
70 
71   override val exponentPackageDelegate: ExponentPackageDelegate? = null
72 
73   private var nuxOverlayView: ReactUnthemedRootView? = null
74   private var notification: ExponentNotification? = null
75   private var tempNotification: ExponentNotification? = null
76   private var isShellApp = false
77   protected var intentUri: String? = null
78   private var isReadyForBundle = false
79   private var notificationRemoteViews: RemoteViews? = null
80   private var notificationBuilder: NotificationCompat.Builder? = null
81   private var isLoadExperienceAllowedToRun = false
82   private var shouldShowLoadingViewWithOptimisticManifest = false
83 
84   /**
85    * Controls loadingProgressPopupWindow that is shown above whole activity.
86    */
87   lateinit var loadingProgressPopupController: LoadingProgressPopupController
88   var managedAppSplashScreenViewProvider: ManagedAppSplashScreenViewProvider? = null
89 
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.instance.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: Manifest) {
175             Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) }
176           }
177 
178           override fun onManifestCompleted(manifest: Manifest) {
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.AnalyticsEvent.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: Manifest?) {
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: Manifest) {
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: Manifest,
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     exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!)
432 
433     // Notifications logic uses this to determine which experience to route a notification to
434     ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl))
435 
436     ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!)
437 
438     val task = kernel.getExperienceActivityTask(this.manifestUrl!!)
439     task.taskId = taskId
440     task.experienceActivity = WeakReference(this)
441     task.activityId = activityId
442     task.bundleUrl = bundleUrl
443 
444     sdkVersion = manifest.getSDKVersion()
445     isShellApp = this.manifestUrl == Constants.INITIAL_URL
446 
447     // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION
448     // to point to the unversioned code in ReactAndroid.
449     if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) {
450       sdkVersion = RNObject.UNVERSIONED
451     }
452 
453     // In detach/shell, we always use UNVERSIONED as the ABI.
454     detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion
455 
456     if (RNObject.UNVERSIONED != sdkVersion) {
457       var isValidVersion = false
458       for (version in Constants.SDK_VERSIONS_LIST) {
459         if (version == sdkVersion) {
460           isValidVersion = true
461           break
462         }
463       }
464       if (!isValidVersion) {
465         KernelProvider.instance.handleError(
466           sdkVersion + " is not a valid SDK version. Options are " +
467             TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "."
468         )
469         return
470       }
471     }
472 
473     soLoaderInit()
474 
475     try {
476       experienceKey = ExperienceKey.fromManifest(manifest)
477       AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY)
478     } catch (e: JSONException) {
479       KernelProvider.instance.handleError("No ID found in manifest.")
480       return
481     }
482 
483     isCrashed = false
484 
485     Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion)
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     runOnUiThread {
509       if (!isInForeground) {
510         return@runOnUiThread
511       }
512       if (reactInstanceManager.isNotNull) {
513         reactInstanceManager.onHostDestroy()
514         reactInstanceManager.assign(null)
515       }
516 
517       reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView")
518       reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity)
519       setReactRootView((reactRootView.get() as View))
520 
521       if (isDebugModeEnabled) {
522         notification = notificationObject
523         jsBundlePath = ""
524         startReactInstance()
525       } else {
526         tempNotification = notificationObject
527         isReadyForBundle = true
528         AsyncCondition.notify(READY_FOR_BUNDLE)
529       }
530 
531       ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity)
532       ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity)
533       ExperienceActivityUtils.setTaskDescription(
534         exponentManifest,
535         manifest,
536         this@ExperienceActivity
537       )
538       showOrReconfigureManagedAppSplashScreen(manifest)
539       setLoadingProgressStatusIfEnabled()
540     }
541   }
542 
543   fun setBundle(localBundlePath: String?) {
544     // by this point, setManifest should have also been called, so prevent
545     // setOptimisticManifest from showing a rogue splash screen
546     shouldShowLoadingViewWithOptimisticManifest = false
547     if (!isDebugModeEnabled) {
548       val finalIsReadyForBundle = isReadyForBundle
549       AsyncCondition.wait(
550         READY_FOR_BUNDLE,
551         object : AsyncConditionListener {
552           override fun isReady(): Boolean {
553             return finalIsReadyForBundle
554           }
555 
556           override fun execute() {
557             notification = tempNotification
558             tempNotification = null
559             jsBundlePath = localBundlePath
560             startReactInstance()
561             AsyncCondition.remove(READY_FOR_BUNDLE)
562           }
563         }
564       )
565     }
566   }
567 
568   fun onEventMainThread(event: ReceivedNotificationEvent) {
569     // TODO(wschurman): investigate removal, this probably is no longer used
570     if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) {
571       try {
572         val rctDeviceEventEmitter =
573           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
574         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
575         reactInstanceManager.callRecursive("getCurrentReactContext")!!
576           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
577           .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received"))
578       } catch (e: Throwable) {
579         EXL.e(TAG, e)
580       }
581     }
582   }
583 
584   fun handleOptions(options: ExperienceOptions) {
585     try {
586       val uri = options.uri
587       if (uri !== null) {
588         handleUri(uri)
589         val rctDeviceEventEmitter =
590           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
591         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
592         reactInstanceManager.callRecursive("getCurrentReactContext")!!
593           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
594           .call("emit", "Exponent.openUri", uri)
595         BranchManager.handleLink(this, uri, detachSdkVersion)
596       }
597       if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) {
598         val rctDeviceEventEmitter =
599           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
600         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
601         reactInstanceManager.callRecursive("getCurrentReactContext")!!
602           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
603           .call(
604             "emit",
605             "Exponent.notification",
606             options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected")
607           )
608       }
609     } catch (e: Throwable) {
610       EXL.e(TAG, e)
611     }
612   }
613 
614   private fun handleUri(uri: String) {
615     // Emits a "url" event to the Linking event emitter
616     val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
617     super.onNewIntent(intent)
618   }
619 
620   fun emitUpdatesEvent(params: JSONObject) {
621     KernelProvider.instance.addEventForExperience(
622       manifestUrl!!,
623       KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString())
624     )
625   }
626 
627   override val isDebugModeEnabled: Boolean
628     get() = manifest?.isDevelopmentMode() ?: false
629 
630   override fun startReactInstance() {
631     Exponent.instance
632       .testPackagerStatus(
633         isDebugModeEnabled, manifest!!,
634         object : Exponent.PackagerStatusCallback {
635           override fun onSuccess() {
636             reactInstanceManager = startReactInstance(
637               this@ExperienceActivity,
638               intentUri,
639               detachSdkVersion,
640               notification,
641               isShellApp,
642               reactPackages(),
643               expoPackages(),
644               devBundleDownloadProgressListener
645             )
646           }
647 
648           override fun onFailure(errorMessage: String) {
649             KernelProvider.instance.handleError(errorMessage)
650           }
651         }
652       )
653   }
654 
655   override fun handleUnreadNotifications(unreadNotifications: JSONArray) {
656     PushNotificationHelper.instance.removeNotifications(this, unreadNotifications)
657   }
658 
659   /*
660    *
661    * Notification
662    *
663    */
664   private fun addNotification() {
665     if (isShellApp || manifestUrl == null || manifest == null) {
666       return
667     }
668 
669     val name = manifest!!.getName() ?: return
670 
671     val remoteViews = RemoteViews(packageName, R.layout.notification)
672     remoteViews.setCharSequence(R.id.home_text_button, "setText", name)
673 
674     // Home
675     val homeIntent = Intent(this, LauncherActivity::class.java)
676     remoteViews.setOnClickPendingIntent(
677       R.id.home_image_button,
678       PendingIntent.getActivity(
679         this, 0,
680         homeIntent, 0
681       )
682     )
683 
684     // Reload
685     remoteViews.setOnClickPendingIntent(
686       R.id.reload_button,
687       PendingIntent.getService(
688         this, 0,
689         ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT
690       )
691     )
692 
693     notificationRemoteViews = remoteViews
694 
695     // Build the actual notification
696     val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
697     notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID)
698 
699     ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel()
700     notificationBuilder =
701       NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID)
702         .setContent(notificationRemoteViews)
703         .setSmallIcon(R.drawable.notification_icon)
704         .setShowWhen(false)
705         .setOngoing(true)
706         .setPriority(Notification.PRIORITY_MAX)
707         .setColor(ContextCompat.getColor(this, R.color.colorPrimary))
708 
709     notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build())
710   }
711 
712   fun removeNotification() {
713     notificationRemoteViews = null
714     notificationBuilder = null
715     removeNotification(this)
716   }
717 
718   fun onNotificationAction() {
719     dismissNuxViewIfVisible(true)
720   }
721 
722   /**
723    * @param isFromNotification true if this is the result of the user taking an
724    * action in the notification view.
725    */
726   fun dismissNuxViewIfVisible(isFromNotification: Boolean) {
727     if (nuxOverlayView == null) {
728       return
729     }
730 
731     runOnUiThread {
732       val fadeOut: Animation = AlphaAnimation(1f, 0f).apply {
733         interpolator = AccelerateInterpolator()
734         duration = 500
735         setAnimationListener(object : Animation.AnimationListener {
736           override fun onAnimationEnd(animation: Animation) {
737             if (nuxOverlayView!!.parent != null) {
738               (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView)
739             }
740             nuxOverlayView = null
741             val eventProperties = JSONObject()
742             try {
743               eventProperties.put("IS_FROM_NOTIFICATION", isFromNotification)
744             } catch (e: JSONException) {
745               EXL.e(TAG, e.message)
746             }
747             Analytics.logEvent(Analytics.AnalyticsEvent.NUX_EXPERIENCE_OVERLAY_DISMISSED, eventProperties)
748           }
749 
750           override fun onAnimationRepeat(animation: Animation) {}
751           override fun onAnimationStart(animation: Animation) {}
752         })
753       }
754       nuxOverlayView!!.startAnimation(fadeOut)
755     }
756   }
757 
758   /*
759    *
760    * Errors
761    *
762    */
763   override fun onError(intent: Intent) {
764     if (manifestUrl != null) {
765       intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl)
766     }
767   }
768 
769   companion object {
770     private val TAG = ExperienceActivity::class.java.simpleName
771     private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad"
772     const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101
773     private const val READY_FOR_BUNDLE = "readyForBundle"
774 
775     /**
776      * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user.
777      */
778     var currentActivity: ExperienceActivity? = null
779       private set
780 
781     @JvmStatic fun removeNotification(context: Context) {
782       val notificationManager =
783         context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
784       notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID)
785     }
786   }
787 }
788