Skip to content

Commit

Permalink
[MOBILE-3696] End any active Live Updates that have lost their notifi…
Browse files Browse the repository at this point in the history
…cations (#1266)

* Clear any active LUs with dismissed notifications on init

* Remove LU buttons from sample home fragment
  • Loading branch information
jyaganeh authored May 31, 2023
1 parent 56b5cec commit 69b01f9
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -46,8 +47,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
});
});

setupLiveUpdateTestButtons(binding);

return binding.getRoot();
}

Expand All @@ -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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ internal constructor(
updateLiveActivityEnablement()
}

/** @hide */
override fun onPerformJob(airship: UAirship, jobInfo: JobInfo): JobResult {
return when (jobInfo.action) {
ACTION_UPDATE_CHANNEL ->
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LiveUpdateStateWithContent>

@Transaction
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down

0 comments on commit 69b01f9

Please sign in to comment.