Blog Infos
Author
Published
Topics
Published

 

Intro

At this point we have an infrastructure to robustly show notifications. Now we are ready to declare ‘clients’ for this infrastructure. Initially we decided to support 3 type of notifications sources :

  • Remote
  • Triggered
  • Scheduled

This final chapter is unveiling implementation details for them.

Notification sources components is high-level design

Remote

 

Remote notifications source

 

Remote notifications are traditionally managed by implementation of some Service from your push notification vendor. It could be Google with Firebase Cloud Messaging or Huawei with Huawei Messaging Service or any other vendor you prefer. It could be even your own in-house built service.

As a singular entry point to notifications framework is a NotificationQueue, then we are pretty flexible to choose/switch any implementation details (like a cloud messaging vendor).

// :remote-notifications <1>
class RemoteMessagingService : FirebaseMessagingService() {

  @Inject
  lateinit var notificationMetadataProvider: NotificationMetadataProvider

  @Inject
  lateinit var notificationQueue: NotificationQueue

  @Inject
  lateinit var context: Context

  override fun onMessageReceived(message: RemoteMessage) {
    val remoteData = message.data

    if (remoteData.isNotEmpty()) { // <2>
      val iconUrl = remoteData["icon"]!!
      val title = remoteData["titleText"]!!
      val description = remoteData["description"]!!
      val textColor = remoteData["fontColor"]!!
      val gradientStartColor = remoteData["bgColorFrom"]!!
      val gradientEndColor = remoteData["bgColorTo"]!!
      val action = when (remoteData["action"]) {
        "memory" -> ACTION_OPEN_MEMORY // <3>
        "battery" -> ACTION_OPEN_BATTERY
        "threats" -> ACTION_OPEN_THREAT_PROTECTION
        "antihack" -> ACTION_OPEN_ANTIHACK
        else -> null
      }

      val data = NotificationData(
        id = notificationMetadataProvider.getIdFor(this::class), // <4>
        channel = NotificationData.Channel.REMOTE,
        type = NotificationData.Type.SYSTEM,
        icon = NotificationData.Icon.Url(iconUrl),
        title = title,
        description = description,
        backgroundGradientStart = Color.parseColor(gradientStartColor),
        backgroundGradientEnd = Color.parseColor(gradientEndColor),
        textColor = Color.parseColor(textColor),
        action = action
      )

      val loadIconRequest = ImageRequest.Builder(context) // <5>
        .data(iconUrl)
        .target {
          runBlocking(Dispatchers.IO) { notificationQueue.add(data) }
        }
        .build()

      ImageLoader(context).enqueue(loadIconRequest)
    }
  }
  1. Remote notification is placed in a separate module. I’m considering it as a standalone feature of app
  2. Parsing remoteData and extracting fields for NotificationData
  3. Action for each feature is declared in a dedicated feature module. See further
  4. Getting a stable notification Id
  5. As we getting url to icon from remote, it’s need to be loaded first, and only then we able to show notification

 

Updated integration structure

 

As actions, that representing navigation to particular feature are declared in proprietary feature module, then :remote-notifications have to depend on it.

The common rule of scalability here is to prohibit features to depend on each other. Instead each feature could declare a thin API module with a very restricted amount of data. Usually it’s interfaces, constants, event buses etc.

Then the feature module declares its api module as an API configuration dependency to let its clients (which is typically :app only) to know about it.

Finally features can depend on API modules or other features.

Triggered

Triggered notifications source

 

Triggered notifications are reactions at some events in the system. It could be:

  • New app install
  • Power Charging status changing
  • Wi-Fi connection establishment
  • any other events you can subscribe to.

Typically observers of these events are BroadcastReceivers, but in the case of your own events — it can be implemented in different ways.

So there is no special abstractions for triggers. As a before, it’s needed only NotificationQueue as a API of notifications framework.

Triggers can be either declared in a related feature module or even in a separate module if it contains a big piece of logic.

Scheduled

 

Scheduled notifications source

 

Notifications that are scheduled to show within some period. Also it is reasonable to apply some conditions to showing. Let’s consider a case, when in some interval each feature wants to show a notification that engages the user to interact with it. If a user interacts with a feature within this interval notification shouldn’t be shown.

Schedule
// :notifications-api
interface NotificationSchedule {

 suspend fun update() // <1>

 suspend fun getLastUpdateTimestamp(): Long // <2>

 suspend fun getNotification(): NotificationData // <3>
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

  1. API for updating a schedule
  2. We are returning last update timestamp from each schedule to have some singular logic (some Scheduler) that gathers all the timestamps and deciding which notifications should be shown
  3. Notification to be shown to engage a user

Each feature that is interested in such a way of showing logic should provide an implementation of this interface:

// :battery
internal val BATTERY_OPTIMIZED_AT_KEY = longPreferencesKey("battery_optimized_at")

class BatteryNotificationsSchedule @Inject constructor(
 private val context: Context,
 private val preferences : DataStore<Preferences>,
 private val notificationMetadataProvider: NotificationMetadataProvider
) : NotificationSchedule {

 private val notification = NotificationData(
    id = notificationMetadataProvider.getIdFor(this::class),
    channel = NotificationData.Channel.SCHEDULED,
    type = NotificationData.Type.FULLSCREEN,
    icon = NotificationData.Icon.Res(R.drawable.ic_battery),
    title = context.getString(R.string.battery_scheduled_notification_title),
    description = context.getString(R.string.notification_description),
    action = ACTION_OPEN_BATTERY,
  )

  override suspend fun update() {
    preferences.edit {
      it[BATTERY_OPTIMIZED_AT_KEY] = System.currentTimeMillis() // <1>
    }
    notificationQueue.remove(notification) // <2>
  }

  override suspend fun getLastUpdateTimestamp(): Long =
    preferences.data.first()[BATTERY_OPTIMIZED_AT_KEY] ?: 0L

  override suspend fun getNotification(): NotificationData = notification
  1. Storing last update timestamp in SharedPreferences
  2. If the notification show was suspended (it came to the queue in the middle of the polling interval) and the schedule was updated before notification shown then we should remove notification from the queue since it’s not actual anymore.
Scheduler

Scheduler is a class which manages the interval of scheduling, collecting all the schedules and choosing which notification should be shown. Managing the interval can be implemented via AlarmLoopReceiver.

// :notifications
private const val ACTION_SCHEDULES_POLL = "schedule_poll"

internal class NotificationsScheduler : AlarmLoopReceiver() {

 @Inject
 lateinit var schedules: Set<NotificationSchedule> // <1>

 @Inject
 lateinit var queue: NotificationQueue

 @Inject
 lateinit var config: NotificationsConfig

 @Inject
 override lateinit var alarmManager: AlarmManager

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

 override val loopAction: String
   get() = ACTION_SCHEDULES_POLL

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

   runBlocking(Dispatchers.IO) {

     val notification =
       schedules
         .filter { System.currentTimeMillis() - it.getLastUpdateTimestamp() > loopPeriod } // <2>
         .map { it.getNotification() }
         .randomOrNull() // <3>

     if (notification != null) {
       queue.add(notification)
     }
   }

   loop(context)
 }
}
  1. We have to provide a set of all schedules somehow. Some DI frameworks provide multibindings feature, some don’t and you have to provide this set to DI tree by hand
  2. Once within loopPeriod we are getting all schedules which update timestamp is too old
  3. And if there are few of them then choose randomly which notification should be shown

As a AlarmLoopReceiver this scheduler will robustly starts at BOOT_COMPLETED event or at app startup.

Conclusion

Notifications framework

 

Eventually, we came up with a notifications framework that focused mostly on reliability. Notifications are able to be queued, and the queue is persistent. Polling of the queue is starting at device boot or at app startup.
It’s able to show high-importance fullscreen notifications at different API levels.
It has a singular entry point that allows it to be abstracted from the implementation details of your app and just keep robustly doing its work — showing notifications.

This article was originally published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In Chapter 1 we have chosen a Looper and Queue as a high-level design…
READ MORE
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
Menu