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   var managedAppSplashScreenViewController: ManagedAppSplashScreenViewController? = null
90 
91   @Inject
92   lateinit var exponentManifest: ExponentManifest
93 
94   @Inject
95   lateinit var devMenuManager: DevMenuManager
96 
97   private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener =
98     object : DevBundleDownloadProgressListener {
99       override fun onProgress(status: String?, done: Int?, total: Int?) {
100         UiThreadUtil.runOnUiThread {
101           loadingProgressPopupController.updateProgress(
102             status,
103             done,
104             total
105           )
106         }
107       }
108 
109       override fun onSuccess() {
110         UiThreadUtil.runOnUiThread {
111           loadingProgressPopupController.hide()
112           managedAppSplashScreenViewController?.startSplashScreenWarningTimer()
113           finishLoading()
114         }
115       }
116 
117       override fun onFailure(error: Exception) {
118         UiThreadUtil.runOnUiThread {
119           loadingProgressPopupController.hide()
120           interruptLoading()
121         }
122       }
123     }
124 
125   /*
126    *
127    * Lifecycle
128    *
129    */
130   override fun onCreate(savedInstanceState: Bundle?) {
131     super.onCreate(savedInstanceState)
132 
133     isLoadExperienceAllowedToRun = true
134     shouldShowLoadingViewWithOptimisticManifest = true
135     loadingProgressPopupController = LoadingProgressPopupController(this)
136 
137     NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this)
138     EventBus.getDefault().registerSticky(this)
139 
140     activityId = ExpoActivityIds.getNextAppActivityId()
141 
142     // TODO: audit this now that kernel logic is on the native side in Kotlin
143     var shouldOpenImmediately = true
144 
145     // If our activity was killed for memory reasons or because of "Don't keep activities",
146     // try to reload manifest using the savedInstanceState
147     if (savedInstanceState != null) {
148       val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY)
149       if (manifestUrl != null) {
150         this.manifestUrl = manifestUrl
151       }
152     }
153 
154     // On cold boot to experience, we're given this information from the Kotlin kernel, instead of
155     // the JS kernel.
156     val bundle = intent.extras
157     if (bundle != null && this.manifestUrl == null) {
158       val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY)
159       if (manifestUrl != null) {
160         this.manifestUrl = manifestUrl
161       }
162 
163       // Don't want to get here if savedInstanceState has manifestUrl. Only care about
164       // IS_OPTIMISTIC_KEY the first time onCreate is called.
165       val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY)
166       if (isOptimistic) {
167         shouldOpenImmediately = false
168       }
169     }
170 
171     if (this.manifestUrl != null && shouldOpenImmediately) {
172       val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false)
173       ExpoUpdatesAppLoader(
174         this.manifestUrl!!,
175         object : AppLoaderCallback {
176           override fun onOptimisticManifest(optimisticManifest: Manifest) {
177             Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) }
178           }
179 
180           override fun onManifestCompleted(manifest: Manifest) {
181             Exponent.instance.runOnUiThread {
182               try {
183                 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL())
184                 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl)
185               } catch (e: JSONException) {
186                 kernel.handleError(e)
187               }
188             }
189           }
190 
191           override fun onBundleCompleted(localBundlePath: String) {
192             Exponent.instance.runOnUiThread { setBundle(localBundlePath) }
193           }
194 
195           override fun emitEvent(params: JSONObject) {
196             emitUpdatesEvent(params)
197           }
198 
199           override fun updateStatus(status: AppLoaderStatus) {
200             setLoadingProgressStatusIfEnabled(status)
201           }
202 
203           override fun onError(e: Exception) {
204             Exponent.instance.runOnUiThread { kernel.handleError(e) }
205           }
206         },
207         forceCache
208       ).start(this)
209     }
210     kernel.setOptimisticActivity(this, taskId)
211   }
212 
213   override fun onResume() {
214     super.onResume()
215     currentActivity = this
216 
217     // Resume home's host if needed.
218     devMenuManager.maybeResumeHostWithActivity(this)
219 
220     soLoaderInit()
221 
222     addNotification()
223     Analytics.logEventWithManifestUrl(Analytics.AnalyticsEvent.EXPERIENCE_APPEARED, manifestUrl)
224   }
225 
226   override fun onWindowFocusChanged(hasFocus: Boolean) {
227     super.onWindowFocusChanged(hasFocus)
228     // Check for manifest to avoid calling this when first loading an experience
229     if (hasFocus && manifest != null) {
230       runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) }
231     }
232   }
233 
234   private fun soLoaderInit() {
235     if (detachSdkVersion != null) {
236       SoLoader.init(this, false)
237     }
238   }
239 
240   open fun shouldCheckOptions() {
241     if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) {
242       handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!)
243     }
244   }
245 
246   override fun onPause() {
247     super.onPause()
248     if (currentActivity === this) {
249       currentActivity = null
250     }
251     removeNotification()
252     Analytics.clearTimedEvents()
253   }
254 
255   public override fun onSaveInstanceState(savedInstanceState: Bundle) {
256     savedInstanceState.putString(KernelConstants.MANIFEST_URL_KEY, manifestUrl)
257     super.onSaveInstanceState(savedInstanceState)
258   }
259 
260   override fun onNewIntent(intent: Intent) {
261     super.onNewIntent(intent)
262     val uri = intent.data
263     if (uri != null) {
264       handleUri(uri.toString())
265     }
266   }
267 
268   fun toggleDevMenu(): Boolean {
269     if (reactInstanceManager.isNotNull && !isCrashed) {
270       devMenuManager.toggleInActivity(this)
271       return true
272     }
273     return false
274   }
275 
276   /**
277    * Handles command line command `adb shell input keyevent 82` that toggles the dev menu on the current experience activity.
278    */
279   override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
280     if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager.isNotNull && !isCrashed) {
281       devMenuManager.toggleInActivity(this)
282       return true
283     }
284     return super.onKeyUp(keyCode, event)
285   }
286 
287   /**
288    * Closes the dev menu when pressing back button when it is visible on this activity.
289    */
290   override fun onBackPressed() {
291     if (currentActivity === this && devMenuManager.isShownInActivity(this)) {
292       devMenuManager.requestToClose(this)
293       return
294     }
295     super.onBackPressed()
296   }
297 
298   fun onEventMainThread(event: KernelStartedRunningEvent?) {
299     AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY)
300   }
301 
302   override fun onDoneLoading() {
303     Analytics.markEvent(Analytics.TimedEvent.FINISHED_LOADING_REACT_NATIVE)
304     Analytics.sendTimedEvents(manifestUrl)
305   }
306 
307   fun onEvent(event: ExperienceDoneLoadingEvent) {
308     if (event.activity === this) {
309       loadingProgressPopupController.hide()
310     }
311 
312     if (!Constants.isStandaloneApp()) {
313       val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
314       if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) {
315         AlertDialog.Builder(this@ExperienceActivity)
316           .setTitle("Using a cached project")
317           .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.")
318           .setPositiveButton("Use cache", null)
319           .setNegativeButton("Reload") { _, _ ->
320             kernel.reloadVisibleExperience(
321               manifestUrl!!, false
322             )
323           }
324           .show()
325       }
326     }
327   }
328 
329   /*
330    *
331    * Experience Loading
332    *
333    */
334   fun startLoading() {
335     isLoading = true
336     showOrReconfigureManagedAppSplashScreen(manifest)
337     setLoadingProgressStatusIfEnabled()
338   }
339 
340   /**
341    * This method is being called twice:
342    * - first time for optimistic manifest
343    * - seconds time for real manifest
344    */
345   protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) {
346     if (!shouldCreateLoadingView()) {
347       return
348     }
349 
350     hideLoadingView()
351     if (managedAppSplashScreenViewProvider == null) {
352       val config = ManagedAppSplashScreenConfiguration.parseManifest(
353         manifest!!
354       )
355       managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config)
356       val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this)
357       managedAppSplashScreenViewController = ManagedAppSplashScreenViewController(
358         this,
359         getRootViewClass(
360           manifest
361         ),
362         splashScreenView
363       )
364       SplashScreen.show(this, managedAppSplashScreenViewController!!, true)
365     } else {
366       managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!)
367     }
368   }
369 
370   fun setLoadingProgressStatusIfEnabled() {
371     val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
372     if (appLoader != null) {
373       setLoadingProgressStatusIfEnabled(appLoader.status)
374     }
375   }
376 
377   fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) {
378     if (Constants.isStandaloneApp()) {
379       return
380     }
381     if (status == null) {
382       return
383     }
384     val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
385     if (appLoader != null && appLoader.shouldShowAppLoaderStatus) {
386       UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) }
387     } else {
388       UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() }
389     }
390   }
391 
392   fun setOptimisticManifest(optimisticManifest: Manifest) {
393     runOnUiThread {
394       if (!isInForeground) {
395         return@runOnUiThread
396       }
397       if (!shouldShowLoadingViewWithOptimisticManifest) {
398         return@runOnUiThread
399       }
400       ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity)
401       ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity)
402       ExperienceActivityUtils.setTaskDescription(
403         exponentManifest,
404         optimisticManifest,
405         this@ExperienceActivity
406       )
407       showOrReconfigureManagedAppSplashScreen(optimisticManifest)
408       setLoadingProgressStatusIfEnabled()
409     }
410   }
411 
412   fun setManifest(
413     manifestUrl: String,
414     manifest: Manifest,
415     bundleUrl: String
416   ) {
417     if (!isInForeground) {
418       return
419     }
420     if (!isLoadExperienceAllowedToRun) {
421       return
422     }
423 
424     // Only want to run once per onCreate. There are some instances with ShellAppActivity where this would be called
425     // twice otherwise. Turn on "Don't keep activities", trigger a notification, background the app, and then
426     // press on the notification in a shell app to see this happen.
427     isLoadExperienceAllowedToRun = false
428 
429     isReadyForBundle = false
430     this.manifestUrl = manifestUrl
431     this.manifest = manifest
432 
433     exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!)
434 
435     // Notifications logic uses this to determine which experience to route a notification to
436     ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl))
437 
438     ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!)
439 
440     val task = kernel.getExperienceActivityTask(this.manifestUrl!!)
441     task.taskId = taskId
442     task.experienceActivity = WeakReference(this)
443     task.activityId = activityId
444     task.bundleUrl = bundleUrl
445 
446     sdkVersion = manifest.getSDKVersion()
447     isShellApp = this.manifestUrl == Constants.INITIAL_URL
448 
449     // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION
450     // to point to the unversioned code in ReactAndroid.
451     if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) {
452       sdkVersion = RNObject.UNVERSIONED
453     }
454 
455     // In detach/shell, we always use UNVERSIONED as the ABI.
456     detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion
457 
458     if (RNObject.UNVERSIONED != sdkVersion) {
459       var isValidVersion = false
460       for (version in Constants.SDK_VERSIONS_LIST) {
461         if (version == sdkVersion) {
462           isValidVersion = true
463           break
464         }
465       }
466       if (!isValidVersion) {
467         KernelProvider.instance.handleError(
468           sdkVersion + " is not a valid SDK version. Options are " +
469             TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "."
470         )
471         return
472       }
473     }
474 
475     soLoaderInit()
476 
477     try {
478       experienceKey = ExperienceKey.fromManifest(manifest)
479       AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY)
480     } catch (e: JSONException) {
481       KernelProvider.instance.handleError("No ID found in manifest.")
482       return
483     }
484 
485     isCrashed = false
486 
487     Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion)
488 
489     ExperienceActivityUtils.updateOrientation(this.manifest!!, this)
490     ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest!!, this)
491     ExperienceActivityUtils.overrideUiMode(this.manifest!!, this)
492 
493     addNotification()
494 
495     var notificationObject: ExponentNotification? = null
496     // Activity could be restarted due to Dark Mode change, only pop options if that will not happen
497     if (kernel.hasOptionsForManifestUrl(manifestUrl)) {
498       val options = kernel.popOptionsForManifestUrl(manifestUrl)
499 
500       // if the kernel has an intent for our manifest url, that's the intent that triggered
501       // the loading of this experience.
502       if (options!!.uri != null) {
503         intentUri = options.uri
504       }
505       notificationObject = options.notificationObject
506     }
507 
508     BranchManager.handleLink(this, intentUri, detachSdkVersion)
509 
510     runOnUiThread {
511       if (!isInForeground) {
512         return@runOnUiThread
513       }
514       if (reactInstanceManager.isNotNull) {
515         reactInstanceManager.onHostDestroy()
516         reactInstanceManager.assign(null)
517       }
518 
519       reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView")
520       reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity)
521       setReactRootView((reactRootView.get() as View))
522 
523       if (isDebugModeEnabled) {
524         notification = notificationObject
525         jsBundlePath = ""
526         startReactInstance()
527       } else {
528         tempNotification = notificationObject
529         isReadyForBundle = true
530         AsyncCondition.notify(READY_FOR_BUNDLE)
531       }
532 
533       ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity)
534       ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity)
535       ExperienceActivityUtils.setTaskDescription(
536         exponentManifest,
537         manifest,
538         this@ExperienceActivity
539       )
540       showOrReconfigureManagedAppSplashScreen(manifest)
541       setLoadingProgressStatusIfEnabled()
542     }
543   }
544 
545   fun setBundle(localBundlePath: String) {
546     // by this point, setManifest should have also been called, so prevent
547     // setOptimisticManifest from showing a rogue splash screen
548     shouldShowLoadingViewWithOptimisticManifest = false
549     if (!isDebugModeEnabled) {
550       val finalIsReadyForBundle = isReadyForBundle
551       AsyncCondition.wait(
552         READY_FOR_BUNDLE,
553         object : AsyncConditionListener {
554           override fun isReady(): Boolean {
555             return finalIsReadyForBundle
556           }
557 
558           override fun execute() {
559             notification = tempNotification
560             tempNotification = null
561             jsBundlePath = localBundlePath
562             startReactInstance()
563             AsyncCondition.remove(READY_FOR_BUNDLE)
564           }
565         }
566       )
567     }
568   }
569 
570   fun onEventMainThread(event: ReceivedNotificationEvent) {
571     // TODO(wschurman): investigate removal, this probably is no longer used
572     if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) {
573       try {
574         val rctDeviceEventEmitter =
575           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
576         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
577         reactInstanceManager.callRecursive("getCurrentReactContext")!!
578           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
579           .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received"))
580       } catch (e: Throwable) {
581         EXL.e(TAG, e)
582       }
583     }
584   }
585 
586   fun handleOptions(options: ExperienceOptions) {
587     try {
588       val uri = options.uri
589       if (uri !== null) {
590         handleUri(uri)
591         val rctDeviceEventEmitter =
592           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
593         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
594         reactInstanceManager.callRecursive("getCurrentReactContext")!!
595           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
596           .call("emit", "Exponent.openUri", uri)
597         BranchManager.handleLink(this, uri, detachSdkVersion)
598       }
599       if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) {
600         val rctDeviceEventEmitter =
601           RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
602         rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
603         reactInstanceManager.callRecursive("getCurrentReactContext")!!
604           .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!!
605           .call(
606             "emit",
607             "Exponent.notification",
608             options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected")
609           )
610       }
611     } catch (e: Throwable) {
612       EXL.e(TAG, e)
613     }
614   }
615 
616   private fun handleUri(uri: String) {
617     // Emits a "url" event to the Linking event emitter
618     val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
619     super.onNewIntent(intent)
620   }
621 
622   fun emitUpdatesEvent(params: JSONObject) {
623     KernelProvider.instance.addEventForExperience(
624       manifestUrl!!,
625       KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString())
626     )
627   }
628 
629   override val isDebugModeEnabled: Boolean
630     get() = manifest?.isDevelopmentMode() ?: false
631 
632   override fun startReactInstance() {
633     Exponent.instance
634       .testPackagerStatus(
635         isDebugModeEnabled, manifest!!,
636         object : Exponent.PackagerStatusCallback {
637           override fun onSuccess() {
638             reactInstanceManager = startReactInstance(
639               this@ExperienceActivity,
640               intentUri,
641               detachSdkVersion,
642               notification,
643               isShellApp,
644               reactPackages(),
645               expoPackages(),
646               devBundleDownloadProgressListener
647             )
648           }
649 
650           override fun onFailure(errorMessage: String) {
651             KernelProvider.instance.handleError(errorMessage)
652           }
653         }
654       )
655   }
656 
657   override fun handleUnreadNotifications(unreadNotifications: JSONArray) {
658     PushNotificationHelper.instance.removeNotifications(this, unreadNotifications)
659   }
660 
661   /*
662    *
663    * Notification
664    *
665    */
666   private fun addNotification() {
667     if (isShellApp || manifestUrl == null || manifest == null) {
668       return
669     }
670 
671     val name = manifest!!.getName() ?: return
672 
673     val remoteViews = RemoteViews(packageName, R.layout.notification)
674     remoteViews.setCharSequence(R.id.home_text_button, "setText", name)
675 
676     // 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(Analytics.AnalyticsEvent.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