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