<lambda>null1 // 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 com.google.firebase.crashlytics.FirebaseCrashlytics
27 import de.greenrobot.event.EventBus
28 import expo.modules.core.interfaces.Package
29 import expo.modules.manifests.core.Manifest
30 import expo.modules.splashscreen.singletons.SplashScreen
31 import host.exp.exponent.*
32 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback
33 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus
34 import host.exp.exponent.analytics.EXL
35 import host.exp.exponent.branch.BranchManager
36 import host.exp.exponent.di.NativeModuleDepsProvider
37 import host.exp.exponent.experience.loading.LoadingProgressPopupController
38 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenConfiguration
39 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewController
40 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewProvider
41 import host.exp.exponent.kernel.*
42 import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent
43 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions
44 import host.exp.exponent.notifications.*
45 import host.exp.exponent.storage.ExponentDB
46 import host.exp.exponent.storage.ExponentDBObject
47 import host.exp.exponent.utils.AsyncCondition
48 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
49 import host.exp.exponent.utils.ExperienceActivityUtils
50 import host.exp.exponent.utils.ExperienceRTLManager
51 import host.exp.exponent.utils.ExpoActivityIds
52 import host.exp.expoview.Exponent
53 import host.exp.expoview.Exponent.StartReactInstanceDelegate
54 import host.exp.expoview.R
55 import org.json.JSONArray
56 import org.json.JSONException
57 import org.json.JSONObject
58 import versioned.host.exp.exponent.ExponentPackageDelegate
59 import versioned.host.exp.exponent.ReactUnthemedRootView
60 import java.lang.ref.WeakReference
61 import javax.inject.Inject
62 
63 open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDelegate {
64   open fun expoPackages(): List<Package>? {
65     // Experience must pick its own modules in ExponentPackage
66     return null
67   }
68 
69   open fun reactPackages(): List<ReactPackage>? {
70     return null
71   }
72 
73   override val exponentPackageDelegate: ExponentPackageDelegate? = null
74 
75   private var nuxOverlayView: ReactUnthemedRootView? = null
76   private var notification: ExponentNotification? = null
77   private var tempNotification: ExponentNotification? = null
78   private var isShellApp = false
79   protected var intentUri: String? = null
80   private var isReadyForBundle = false
81   private var notificationRemoteViews: RemoteViews? = null
82   private var notificationBuilder: NotificationCompat.Builder? = null
83   private var isLoadExperienceAllowedToRun = false
84   private var shouldShowLoadingViewWithOptimisticManifest = false
85 
86   /**
87    * Controls loadingProgressPopupWindow that is shown above whole activity.
88    */
89   lateinit var loadingProgressPopupController: LoadingProgressPopupController
90   var managedAppSplashScreenViewProvider: ManagedAppSplashScreenViewProvider? = null
91   var managedAppSplashScreenViewController: ManagedAppSplashScreenViewController? = null
92 
93   @Inject
94   lateinit var exponentManifest: ExponentManifest
95 
96   @Inject
97   lateinit var devMenuManager: DevMenuManager
98 
99   private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener =
100     object : DevBundleDownloadProgressListener {
101       override fun onProgress(status: String?, done: Int?, total: Int?) {
102         UiThreadUtil.runOnUiThread {
103           loadingProgressPopupController.updateProgress(
104             status,
105             done,
106             total
107           )
108         }
109       }
110 
111       override fun onSuccess() {
112         UiThreadUtil.runOnUiThread {
113           loadingProgressPopupController.hide()
114           managedAppSplashScreenViewController?.startSplashScreenWarningTimer()
115           finishLoading()
116         }
117       }
118 
119       override fun onFailure(error: Exception) {
120         UiThreadUtil.runOnUiThread {
121           loadingProgressPopupController.hide()
122           interruptLoading()
123         }
124       }
125     }
126 
127   /*
128    *
129    * Lifecycle
130    *
131    */
132   override fun onCreate(savedInstanceState: Bundle?) {
133     super.onCreate(savedInstanceState)
134 
135     isLoadExperienceAllowedToRun = true
136     shouldShowLoadingViewWithOptimisticManifest = true
137     loadingProgressPopupController = LoadingProgressPopupController(this)
138 
139     NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this)
140     EventBus.getDefault().registerSticky(this)
141 
142     activityId = ExpoActivityIds.getNextAppActivityId()
143 
144     // TODO: audit this now that kernel logic is on the native side in Kotlin
145     var shouldOpenImmediately = true
146 
147     // If our activity was killed for memory reasons or because of "Don't keep activities",
148     // try to reload manifest using the savedInstanceState
149     if (savedInstanceState != null) {
150       val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY)
151       if (manifestUrl != null) {
152         this.manifestUrl = manifestUrl
153       }
154     }
155 
156     // On cold boot to experience, we're given this information from the Kotlin kernel, instead of
157     // the JS kernel.
158     val bundle = intent.extras
159     if (bundle != null && this.manifestUrl == null) {
160       val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY)
161       if (manifestUrl != null) {
162         this.manifestUrl = manifestUrl
163       }
164 
165       // Don't want to get here if savedInstanceState has manifestUrl. Only care about
166       // IS_OPTIMISTIC_KEY the first time onCreate is called.
167       val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY)
168       if (isOptimistic) {
169         shouldOpenImmediately = false
170       }
171     }
172 
173     FirebaseCrashlytics.getInstance().log("ExperienceActivity.manifestUrl: ${this.manifestUrl}")
174     if (this.manifestUrl != null && shouldOpenImmediately) {
175       val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false)
176       ExpoUpdatesAppLoader(
177         this.manifestUrl!!,
178         object : AppLoaderCallback {
179           override fun onOptimisticManifest(optimisticManifest: Manifest) {
180             Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) }
181           }
182 
183           override fun onManifestCompleted(manifest: Manifest) {
184             Exponent.instance.runOnUiThread {
185               try {
186                 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL())
187                 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl)
188               } catch (e: JSONException) {
189                 kernel.handleError(e)
190               }
191             }
192           }
193 
194           override fun onBundleCompleted(localBundlePath: String) {
195             Exponent.instance.runOnUiThread { setBundle(localBundlePath) }
196           }
197 
198           override fun emitEvent(params: JSONObject) {
199             emitUpdatesEvent(params)
200           }
201 
202           override fun updateStatus(status: AppLoaderStatus) {
203             setLoadingProgressStatusIfEnabled(status)
204           }
205 
206           override fun onError(e: Exception) {
207             Exponent.instance.runOnUiThread { kernel.handleError(e) }
208           }
209         },
210         forceCache
211       ).start(this)
212     }
213     kernel.setOptimisticActivity(this, taskId)
214   }
215 
216   override fun onResume() {
217     super.onResume()
218     currentActivity = this
219 
220     // Resume home's host if needed.
221     devMenuManager.maybeResumeHostWithActivity(this)
222 
223     soLoaderInit()
224 
225     addNotification()
226   }
227 
228   override fun onWindowFocusChanged(hasFocus: Boolean) {
229     super.onWindowFocusChanged(hasFocus)
230     // Check for manifest to avoid calling this when first loading an experience
231     if (hasFocus && manifest != null) {
232       runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) }
233     }
234   }
235 
236   private fun soLoaderInit() {
237     if (detachSdkVersion != null) {
238       SoLoader.init(this, false)
239     }
240   }
241 
242   open fun shouldCheckOptions() {
243     if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) {
244       handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!)
245     }
246   }
247 
248   override fun onPause() {
249     super.onPause()
250     if (currentActivity === this) {
251       currentActivity = null
252     }
253     removeNotification()
254   }
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   }
305 
306   fun onEvent(event: ExperienceDoneLoadingEvent) {
307     if (event.activity === this) {
308       loadingProgressPopupController.hide()
309     }
310 
311     if (!Constants.isStandaloneApp()) {
312       val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
313       if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) {
314         AlertDialog.Builder(this@ExperienceActivity)
315           .setTitle("Using a cached project")
316           .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.")
317           .setPositiveButton("Use cache", null)
318           .setNegativeButton("Reload") { _, _ ->
319             kernel.reloadVisibleExperience(
320               manifestUrl!!, false
321             )
322           }
323           .show()
324       }
325     }
326   }
327 
328   /*
329    *
330    * Experience Loading
331    *
332    */
333   fun startLoading() {
334     isLoading = true
335     showOrReconfigureManagedAppSplashScreen(manifest)
336     setLoadingProgressStatusIfEnabled()
337   }
338 
339   /**
340    * This method is being called twice:
341    * - first time for optimistic manifest
342    * - seconds time for real manifest
343    */
344   protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) {
345     if (!shouldCreateLoadingView()) {
346       return
347     }
348 
349     hideLoadingView()
350     if (managedAppSplashScreenViewProvider == null) {
351       val config = ManagedAppSplashScreenConfiguration.parseManifest(
352         manifest!!
353       )
354       managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config)
355       val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this)
356       managedAppSplashScreenViewController = ManagedAppSplashScreenViewController(
357         this,
358         getRootViewClass(
359           manifest
360         ),
361         splashScreenView
362       )
363       SplashScreen.show(this, managedAppSplashScreenViewController!!, true)
364     } else {
365       managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!)
366     }
367   }
368 
369   fun setLoadingProgressStatusIfEnabled() {
370     val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
371     if (appLoader != null) {
372       setLoadingProgressStatusIfEnabled(appLoader.status)
373     }
374   }
375 
376   fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) {
377     if (Constants.isStandaloneApp()) {
378       return
379     }
380     if (status == null) {
381       return
382     }
383     val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl)
384     if (appLoader != null && appLoader.shouldShowAppLoaderStatus) {
385       UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) }
386     } else {
387       UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() }
388     }
389   }
390 
391   fun setOptimisticManifest(optimisticManifest: Manifest) {
392     runOnUiThread {
393       if (!isInForeground) {
394         return@runOnUiThread
395       }
396       if (!shouldShowLoadingViewWithOptimisticManifest) {
397         return@runOnUiThread
398       }
399       ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity)
400       ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity)
401       ExperienceActivityUtils.setTaskDescription(
402         exponentManifest,
403         optimisticManifest,
404         this@ExperienceActivity
405       )
406       showOrReconfigureManagedAppSplashScreen(optimisticManifest)
407       setLoadingProgressStatusIfEnabled()
408       ExperienceRTLManager.setSupportsRTLFromManifest(this, optimisticManifest)
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.getExpoGoSDKVersion()
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     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     ExperienceRTLManager.setSupportsRTLFromManifest(this, manifest)
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     // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability
677     val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
678 
679     // Home
680     val homeIntent = Intent(this, LauncherActivity::class.java)
681     remoteViews.setOnClickPendingIntent(
682       R.id.home_image_button,
683       PendingIntent.getActivity(
684         this, 0,
685         homeIntent, mutableFlag
686       )
687     )
688 
689     // Reload
690     remoteViews.setOnClickPendingIntent(
691       R.id.reload_button,
692       PendingIntent.getService(
693         this, 0,
694         ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag
695       )
696     )
697 
698     notificationRemoteViews = remoteViews
699 
700     // Build the actual notification
701     val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
702     notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID)
703 
704     ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel()
705     notificationBuilder =
706       NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID)
707         .setContent(notificationRemoteViews)
708         .setSmallIcon(R.drawable.notification_icon)
709         .setShowWhen(false)
710         .setOngoing(true)
711         .setPriority(Notification.PRIORITY_MAX)
712         .setColor(ContextCompat.getColor(this, R.color.colorPrimary))
713 
714     notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build())
715   }
716 
717   fun removeNotification() {
718     notificationRemoteViews = null
719     notificationBuilder = null
720     removeNotification(this)
721   }
722 
723   fun onNotificationAction() {
724     dismissNuxViewIfVisible(true)
725   }
726 
727   /**
728    * @param isFromNotification true if this is the result of the user taking an
729    * action in the notification view.
730    */
731   fun dismissNuxViewIfVisible(isFromNotification: Boolean) {
732     if (nuxOverlayView == null) {
733       return
734     }
735 
736     runOnUiThread {
737       val fadeOut: Animation = AlphaAnimation(1f, 0f).apply {
738         interpolator = AccelerateInterpolator()
739         duration = 500
740         setAnimationListener(object : Animation.AnimationListener {
741           override fun onAnimationEnd(animation: Animation) {
742             if (nuxOverlayView!!.parent != null) {
743               (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView)
744             }
745             nuxOverlayView = null
746           }
747 
748           override fun onAnimationRepeat(animation: Animation) {}
749           override fun onAnimationStart(animation: Animation) {}
750         })
751       }
752       nuxOverlayView!!.startAnimation(fadeOut)
753     }
754   }
755 
756   /*
757    *
758    * Errors
759    *
760    */
761   override fun onError(intent: Intent) {
762     if (manifestUrl != null) {
763       intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl)
764     }
765   }
766 
767   companion object {
768     private val TAG = ExperienceActivity::class.java.simpleName
769     private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad"
770     const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101
771     private const val READY_FOR_BUNDLE = "readyForBundle"
772 
773     /**
774      * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user.
775      */
776     var currentActivity: ExperienceActivity? = null
777       private set
778 
779     @JvmStatic fun removeNotification(context: Context) {
780       val notificationManager =
781         context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
782       notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID)
783     }
784   }
785 }
786