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