Blog Infos
Author
Published
Topics
Author
Published

 

 

Intro

In Chapter 1 we have chosen a Looper and Queue as a high-level design for notifications framework and declared the contracts and public API for it. This chapter is focused on Looper and Queue implementation details.

 

Looper and queue components in high-level design

 

Looper
// :notifications
internal const val ACTION_NOTIFICATION_POLL = "notification_poll" // <1>

internal class NotificationPoller : AlarmLoopReceiver() {

 @Inject
 lateinit var config: NotificationsConfig // <2>

 @Inject
 lateinit var queue: NotificationQueue

 @Inject
 override lateinit var alarmManager: AlarmManager

 override val loopPeriod: Long
   get() = config.period

 override val loopAction: String
   get() = ACTION_NOTIFICATION_POLL

 override fun onReceive(context: Context, intent: Intent) {
   super.onReceive(context, intent)
   if (intent.action != ACTION_NOTIFICATION_POLL) return

   runBlocking(Dispatchers.IO) {
     val data = queue.poll()
     if (data != null) { // <3>
       showNotification(data)
       loop(context)
     } else {
       stop(context)
     }
   }
 }

 private fun showNotification(data: NotificationData) {
    // Chapter 3
 }
}
  1. Action for alarm intent that describing notification poll
  2. Simple interface holding different notifications related values
  3. If we poll some data from the queue then we show it and run the loop iteration. Otherwise the queue is empty and we stop the loop.

Register this receiver in AndroidManifest.xml

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

// :notifications
<receiver
   android:name=".NotificationPoller"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.BOOT_COMPLETED" />
   </intent-filter>
</receiver>

It’s important for any AlarmLoopReceiver to be exported because alarms are operating outside the lifetime of application. It is a possible security vulnerability that you should be aware of.

Queue

Chapter 1 is defined a queue contract:

// :notifications-api
interface NotificationQueue {

 suspend fun add(notification: NotificationData)

 suspend fun remove(notification: NotificationData)

 suspend fun poll() : NotificationData?

 suspend fun peek() : NotificationData?
}

Also there are commitments that queue should be persistent and SharedPreferences was chosen as a persistence provider.

// :notifications
private val NOTIFICATIONS_KEY = stringPreferencesKey("notifications") // <1>

class PersistentNotificationQueue @Inject constructor(
 private val store : DataStore<Preferences>
) : NotificationQueue {

 override suspend fun peek(): NotificationData? { // <2>
   val notifications = loadNotifications()
   return notifications.firstOrNull()
 }

 override suspend fun poll(): NotificationData? { // <3>
   val notifications = loadNotifications()
   val first = notifications.firstOrNull()
   if (first != null) {
     store.editNotifications { it.toMutableList().apply { remove(first) } }
   }
   return first
 }

 override suspend fun add(notification: NotificationData) {
   store.editNotifications { it.toMutableList().apply { add(notification) } }
 }

 override suspend fun remove(notification: NotificationData) {
   store.editNotifications { it.toMutableList().apply { remove(notification) } }
 }

 private suspend fun DataStore<Preferences>.editNotifications(transform: (List<NotificationData>) -> List<NotificationData>) {
   val oldNotifications = loadNotifications()
   val newNotifications = transform(oldNotifications)

   edit { preferences ->
     preferences[NOTIFICATIONS_KEY] = Json.encodeToString(newNotifications)
   }
 }

 private suspend fun loadNotifications(): List<NotificationData> { // <4>
   val notificationsJson = store.data.first()[NOTIFICATIONS_KEY]
   return if (notificationsJson == null) emptyList()
   else Json.decodeFromString(notificationsJson)
 }
}
  1. Notifications storing as a Json string in preferences
  2. Peek() returns the first element of the queue but doesn’t remove it.
  3. Poll() returns the first element of the queue and removes it.
  4. Operating over a list and emulating a queue as an access contract. It’s easier.

Now consider the case, when notification was added to an empty queue, and previous notification was shown later than throttling interval. In this case notification should be shown immediately.

This logic could be done via subscription to preferences and placed in NotificationPoller, but I placed this logic to queue directly. I’m using delegation to not overcomplicate logic in PersistantNotificationQueue :

// :notifications
private val LAST_NOTIFICATION_POLL_KEY = longPreferencesKey("last_notification_poll") // <1>

class DefaultNotificationQueue @Inject constructor(
 private val delegate: NotificationQueue, // <2>
 private val config: NotificationsConfig,
 private val store : DataStore<Preferences>
) : NotificationQueue by delegate {

 override suspend fun poll(): NotificationData? {
   val data = delegate.poll()
   if (data != null) {
     updateLastNotificationPoll() // <3>
   }
   return data
 }

 override suspend fun add(notification: NotificationData) {
   val lastNotificationPoll = preferences.data.first()[LAST_NOTIFICATION_POLL_KEY] ?: 0
   val isImmediate =
     System.currentTimeMillis() - lastNotificationPoll > config.period

   if (delegate.peek() == null && isImmediate) { // <4>
     val intent = Intent(context, NotificationPoller::class.java).apply {
       action = ACTION_NOTIFICATION_POLL
     }

     delegate.add(notification)
     context.sendBroadcast(intent)
   } else {
     delegate.add(notification)
   }
 }

 private suspend fun updateLastNotificationPoll() {
   preferences.edit { preferences ->
     preferences[LAST_NOTIFICATION_POLL_KEY] = System.currentTimeMillis()
   }
 }
}
  1. Storing timestamp of last notification poll event
  2. As a PersistentNotificationQueue
  3. If notification was polled then we updating last poll timestamp
  4. Condition for showing notification immediately
Conclusion

This chapter covered implementation details for Looper and Queue. Queue of notifications now is persistent that allows not to lose notifications after device reboot. Looper logic is also robust and launches after device reboot and reasonable in terms of system resources — it doesn’t loop if Queue is empty.
Next chapter will cover notifications appearance.

This article was originally published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Showing notifications is a great mechanism for engaging users at mobile platforms. Smart management…
READ MORE
blog
In Chapter 2 we provided a robust mechanism for queueing notifications. The system is…
READ MORE
blog
At this point we have an infrastructure to robustly show notifications. Now we are…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu