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