13.2. Local notifications

Local notifications are generated from the device itself. There are many situations where they are needed: timers, indicating that a background task has been completed, …

Just like remote notifications, local notifications are organized into channels. For example, we can define these channels in the onCreate callback of our activity.

If (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
   val name = alert_channel
   val descriptionText = error alerts
   val importance = NotificationManager.IMPORTANCE_DEFAULT
   val mChannel = NotificationChannel(alert_channel, name, importance)
   mChannel.description = descriptionText
   val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
   notificationManager.createNotificationChannel(mChannel)
}

In this case we have indicated NotificationManager.IMPORTANCE_DEFAULT which is the default importance of notifications for this channel. If we had assigned NotificationManager.IMPORTANCE_HIGH we would be indicating that we want this notification to always be on screen above everything else. Despite this, it is the user who has the last word about the importance of notifications since they are also set in “Settings \ Apps & Notifications”.

The minimum code for launching a notification when the user taps on an interface element would be the following:

binding.txtHello.setOnClickListener {

   val builder = NotificationCompat.Builder(context, "alert_channel")
      .setSmallIcon(R.drawable.ic_launcher_background)
      .setContentTitle("notification 1")
      .setContentText("Hello from the app!.")

   with(NotificationManagerCompat.from(context)) {
     notify(Random.nextInt(), builder.build())
   }
}

Example of a simple local notification. Example of a simple local notification.
Source: Javier Salvador (Original image) License: CC BY-NC-ND 4.0

For local notifications, we can also add buttons that trigger actions. For example, we are going to create a notification with two buttons: “ok” and “cancel”.

To do this, we use the addAction method that receives a PendingIntent as a parameter. This PendingIntent contains the action and parameters that we want to be executed when the notification button is pressed. A PendingIntent is created because the call will not be made from our own app, but from the system notification inbox (which is managed by Android).

var okIntent = Intent(this, MainActivity::class.java)
okIntent.setAction("okUser")
val okIntentPending = PendingIntent.getActivity(this, System.currentTimeMillis().toInt(), okIntent, 0)

val cancelIntent = Intent(this, MainActivity::class.java)
cancelIntent.setAction("cancelUser")
val cancelIntentPending = PendingIntent.getActivity(this, System.currentTimeMillis().toInt(), cancelIntent, 0)


val builder = NotificationCompat.Builder(this, "alert_channel")
   .setSmallIcon(R.drawable.ic_launcher_background)
   .setVisibility(VISIBILITY_PUBLIC)
   .setContentTitle("notification 2")
   .setContentText("Hello from the app!.")
   .addAction(NotificationCompat.Action(R.drawable.ic_launcher_background, "Ok",okIntentPending))
    .addAction(NotificationCompat.Action(R.drawable.ic_launcher_background, "Cancel", cancelIntentPending))
   .setPriority(IMPORTANCE_HIGH)
   .setDefaults(Notification.DEFAULT_VIBRATE)

with(NotificationManagerCompat.from(this)) {
   notify(Random.nextInt(), builder.build())
}

Example of a local notification with two buttons. Example of a local notification with two buttons.
Source: Javier Salvador (Original image) License: CC BY-NC-ND 4.0

Pressing one of the buttons will execute the class Activity that we have indicated in the addAction method. In the onCreate method of that Activity, we must analyze whether it is a call caused by one of these buttons and act accordingly.

if (getIntent() != null && getIntent().getAction() != null) {
   if (getIntent().getAction()=="okUser") {
       okUser()
   }
   else if (getIntent().getAction()=="cancelUser") {
       cancelUser()
   }
}

As we discussed, one of the common uses of local notifications is to alert the user that a timer has ended. As we have seen in the concurrency section (Section 10.5) we create a Worker instructing the WorkManager to run after a certain amount of time has passed. In the example shown below, we define a time-out after 5 seconds:

val work =
   OneTimeWorkRequestBuilder<MyWorker>()
       .setInitialDelay(5, java.util.concurrent.TimeUnit.SECONDS)
       .addTag("worker_notification")
       .build()

WorkManager.getInstance(this).enqueue(work)

Our MyWorker class will launch the notification.

class MyWorker(val context: Context,
              workerParams: WorkerParameters
) : Worker(context, workerParams) {

   override fun doWork(): Result {

       val builder = NotificationCompat.Builder(context, "alert_channel")
           .setSmallIcon(R.drawable.ic_launcher_background)

           .setContentTitle("notification 1")
           .setContentText("Hello from the app!.")


       with(NotificationManagerCompat.from(context)) {
           notify(Random.nextInt(), builder.build())
       }

       return Result.success()
   }
}

We need to remember that the WorkManager is another process and, therefore, even if we close the application the WorkManager will continue running and send the notification.