From 69b01f9d1a5ba58094b7fc71a3b33f1b272033bc Mon Sep 17 00:00:00 2001 From: Josh Yaganeh <319444+jyaganeh@users.noreply.github.com> Date: Wed, 31 May 2023 16:32:28 -0700 Subject: [PATCH] [MOBILE-3696] End any active Live Updates that have lost their notifications (#1266) * Clear any active LUs with dismissed notifications on init * Remove LU buttons from sample home fragment --- .../sample/home/HomeFragment.java | 68 +------------------ .../liveupdate/LiveUpdateManager.kt | 6 ++ .../liveupdate/LiveUpdateRegistrar.kt | 33 ++++++++- .../liveupdate/data/LiveUpdateDao.kt | 6 +- .../liveupdate/LiveUpdateManagerTest.kt | 4 +- 5 files changed, 41 insertions(+), 76 deletions(-) diff --git a/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java b/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java index 232bfc95a..d02a6ee6b 100644 --- a/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java +++ b/sample/src/main/java/com/urbanairship/sample/home/HomeFragment.java @@ -30,6 +30,7 @@ */ public class HomeFragment extends Fragment { + @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { HomeViewModel viewModel = new ViewModelProvider(this).get(HomeViewModel.class); @@ -46,8 +47,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); }); - setupLiveUpdateTestButtons(binding); - return binding.getRoot(); } @@ -57,69 +56,4 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat Toolbar toolbar = view.findViewById(R.id.toolbar); NavigationUI.setupWithNavController(toolbar, Navigation.findNavController(view)); } - - // TODO: Replace with something less hacky when backend is ready to send real Live Updates. - // Should live update stuff even be on the home screen? Could be in settings instead... - private void setupLiveUpdateTestButtons(FragmentHomeBinding binding) { - AtomicInteger score1 = new AtomicInteger(); - AtomicInteger score2 = new AtomicInteger(); - - // Start button - binding.startLiveUpdate.setOnClickListener(v -> { - JsonMap content = JsonMap.newBuilder() - .put("team_one_score", 0) - .put("team_two_score", 0) - .put("status_update", "Match start!") - .build(); - - LiveUpdateManager.shared().start("foxes-tigers", "sports", content); - }); - - // +1 Foxes button - binding.updateLiveUpdate1.setOnClickListener(v -> { - JsonMap content = JsonMap.newBuilder() - .put("team_one_score", score1.getAndIncrement()) - .put("team_two_score", score2.get()) - .put("status_update", "Foxes score!") - .build(); - - LiveUpdateManager.shared().update("foxes-tigers", content); - }); - - // +1 Tigers button - binding.updateLiveUpdate2.setOnClickListener(v -> { - JsonMap content = JsonMap.newBuilder() - .put("team_one_score", score1.get()) - .put("team_two_score", score2.getAndIncrement()) - .put("status_update", "Tigers score!") - .build(); - - LiveUpdateManager.shared().update("foxes-tigers", content); - }); - - // Stop button - binding.stopLiveUpdate.setOnClickListener(v -> { - int s1 = score1.get(); - int s2 = score2.get(); - String status; - if (s1 == s2) { - status = "It's a tie!"; - } else if (s1 > s2) { - status = "Foxes win!"; - } else { - status = "Tigers win!"; - } - - JsonMap content = JsonMap.newBuilder() - .put("teamOneScore", s1) - .put("team_two_score", s2) - .put("status_update", status) - .build(); - - LiveUpdateManager.shared().end("foxes-tigers", content); - - score1.set(0); - score2.set(0); - }); - } } diff --git a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateManager.kt b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateManager.kt index bf50bd398..ddbb7493c 100644 --- a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateManager.kt +++ b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateManager.kt @@ -162,6 +162,7 @@ internal constructor( updateLiveActivityEnablement() } + /** @hide */ override fun onPerformJob(airship: UAirship, jobInfo: JobInfo): JobResult { return when (jobInfo.action) { ACTION_UPDATE_CHANNEL -> @@ -197,6 +198,11 @@ internal constructor( private fun updateLiveActivityEnablement() { if (isFeatureEnabled) { pushManager.addPushListener(pushListener) + + // Check for any active live Updates that have had their notifications cleared. + // This makes sure we'll end the live update if the notification is dropped due + // to an app upgrade or other cases where we don't get notified of the dismiss. + registrar.stopLiveUpdatesForClearedNotifications() } else { // Clear all live updates. registrar.clearAll() diff --git a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateRegistrar.kt b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateRegistrar.kt index dfb0ac7e1..5dcec0336 100644 --- a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateRegistrar.kt +++ b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/LiveUpdateRegistrar.kt @@ -1,7 +1,10 @@ package com.urbanairship.liveupdate +import android.app.NotificationManager import android.content.Context +import android.content.Context.NOTIFICATION_SERVICE import android.content.Intent +import android.os.Build import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -28,12 +31,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Manages Live Update handlers and an operation queue to process Live Update events. */ internal class LiveUpdateRegistrar( private val context: Context, - dao: LiveUpdateDao, + private val dao: LiveUpdateDao, dispatcher: CoroutineDispatcher = AirshipDispatchers.IO, private val jobDispatcher: JobDispatcher = JobDispatcher.shared(context), private val processor: LiveUpdateProcessor = LiveUpdateProcessor(dao), @@ -134,7 +138,7 @@ internal class LiveUpdateRegistrar( Operation.ClearAll(timestamp = timestamp) ) - internal fun onLiveUpdatePushReceived(message: PushMessage, payload: LiveUpdatePayload) { + fun onLiveUpdatePushReceived(message: PushMessage, payload: LiveUpdatePayload) { with(payload) { when (event) { LiveUpdateEvent.START -> if (type != null) { @@ -148,6 +152,31 @@ internal class LiveUpdateRegistrar( } } + /** + * End any Live Updates notifications that are no longer displayed. + * On API 21 and 22, the notification manager does not provide a way to query for + * active notifications, so this method will no-op. + */ + fun stopLiveUpdatesForClearedNotifications() { + scope.launch { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val nm = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val activeNotifications = nm.activeNotifications.map { it.tag } + + dao.getAllActive() + // Filter out any LUs that use custom handlers or have active notifications + .filter { (update, _) -> + handlers[update.type] is LiveUpdateNotificationHandler && + notificationTag(update.type, update.name) !in activeNotifications + } + // End any Live Updates that are no longer displayed + .forEach { (update, content) -> + stop(update.name, content?.content, update.timestamp, update.dismissalDate) + } + } + } + } + private suspend fun handleCallback(callback: HandlerCallback) { val (action, update, message) = callback val type = update.type diff --git a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDao.kt b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDao.kt index 63cb38d63..f8ed0a01b 100644 --- a/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDao.kt +++ b/urbanairship-live-update/src/main/java/com/urbanairship/liveupdate/data/LiveUpdateDao.kt @@ -43,8 +43,7 @@ internal interface LiveUpdateDao { suspend fun getContent(name: String): LiveUpdateContent? @Transaction - @Query("SELECT * FROM live_update_state WHERE isActive = 1 " + - "AND ((dismissal_date IS NULL) OR (dismissal_date >= strftime('%s', 'now') * 1000))") + @Query("SELECT * FROM live_update_state WHERE isActive = 1") suspend fun getAllActive(): List @Transaction @@ -75,8 +74,7 @@ internal interface LiveUpdateDao { deleteAllContent() } - @Query("SELECT COUNT(*) > 0 FROM live_update_state WHERE isActive = 1 " + - "AND ((dismissal_date IS NULL) OR (dismissal_date >= strftime('%s', 'now') * 1000))") + @Query("SELECT COUNT(*) > 0 FROM live_update_state WHERE isActive = 1") suspend fun isAnyActive(): Boolean @Query("SELECT COUNT(*) FROM live_update_state") diff --git a/urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateManagerTest.kt b/urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateManagerTest.kt index b9cac910b..073535793 100644 --- a/urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateManagerTest.kt +++ b/urbanairship-live-update/src/test/java/com/urbanairship/liveupdate/LiveUpdateManagerTest.kt @@ -1,7 +1,6 @@ package com.urbanairship.liveupdate import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.urbanairship.PreferenceDataStore import com.urbanairship.PrivacyManager import com.urbanairship.TestApplication @@ -33,7 +32,7 @@ public class LiveUpdateManagerTest { private val database: LiveUpdateDatabase = mockk { every { liveUpdateDao() } returns dao } - private val registrar: LiveUpdateRegistrar = mockk() + private val registrar: LiveUpdateRegistrar = mockk(relaxUnitFun = true) private lateinit var dataStore: PreferenceDataStore private lateinit var privacyManager: PrivacyManager @@ -42,7 +41,6 @@ public class LiveUpdateManagerTest { @Before public fun setUp() { - val context = InstrumentationRegistry.getInstrumentation().context dataStore = PreferenceDataStore.inMemoryStore(TestApplication.getApplication()) privacyManager = PrivacyManager(dataStore, PrivacyManager.FEATURE_ALL)