<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