Android provides a feature that enables you to schedule notifications for later. This is useful if users/developers need to do something later in time. For example, if you need the user to remind some task they set in the future in a to-do list application. This blog uses AlarmManager and Broadcast Receivers to achieve the same in android API level 26 or later.
Note → We will not be using the WorkManager API to schedule alarms since alarms are a special use case and are not the part of background work. Also the recommended way to execute exact time jobs is the AlarmManager.
Step 1: Set up the data class:
Since I have used this data class with RoomDatabase it is annotated with Entity but not necessary otherwise. The main thing however is the id field that is a primary key and later will be used to set multiple notifications at a time.
@Entity(tableName = "taskInfo") | |
data class TaskInfo( | |
@PrimaryKey(autoGenerate = false) | |
var id : Int, | |
var description : String, | |
var date : Date, | |
var priority : Int, | |
var status : Boolean, | |
var category: String | |
) : Serializable |
Step 2. Set up the BroadcastReceiver class:
This class will be called by our AlarmManager at some specific time in the future and this class will fire the notification at that instant. We need to add the notification inside this class. Some main things to consider while adding a notification are —
- setContentIntent() = It takes a pending intent that will fire when a user taps the notification. PendingIntent here takes four parameters that are context, requestCode, intent, and a flag. Make sure to add one of the flags as FLAG_IMMUTABLE since it is necessary for the latest APIs.
- setAutoCancel() = It determines whether the notification will disappear or not when the user taps on it.
- tapResultIntent.flags = Make sure to add the intent flag ar your own convenience. I’m using FLAG_ACTIVITY_SINGLE_TOP because this app uses single activity multiple fragment model and at any instance, there exists only MainActivity at the top of the stack and when a user taps the notification when the app is in the foreground, it does not create any new instance of the activity.
- NotificationCompat.Builder() = It takes context and channel_id as parameters and we need to make sure the channel_id is the same as the channel_id we will use later to create the notification channel.
class AlarmReceiver : BroadcastReceiver() { | |
private var notificationManager: NotificationManagerCompat? = null | |
override fun onReceive(p0: Context?, p1: Intent?) { | |
val taskInfo = p1?.getSerializableExtra("task_info") as? TaskInfo | |
// tapResultIntent gets executed when user taps the notification | |
val tapResultIntent = Intent(p0, MainActivity::class.java) | |
tapResultIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP | |
val pendingIntent: PendingIntent = getActivity( p0,0,tapResultIntent,FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) | |
val notification = p0?.let { | |
NotificationCompat.Builder(it, "to_do_list") | |
.setContentTitle("Task Reminder") | |
.setContentText(taskInfo?.description) | |
.setSmallIcon(R.mipmap.ic_launcher) | |
.setAutoCancel(true) | |
.setPriority(NotificationCompat.PRIORITY_HIGH) | |
.setContentIntent(pendingIntent) | |
.build() | |
} | |
notificationManager = p0?.let { NotificationManagerCompat.from(it) } | |
notification?.let { taskInfo?.let { it1 -> notificationManager?.notify(it1.id, it) } } | |
} | |
} |
This class here receives the taskInfo object as intent in order to display the description of the notification as taskInfo.description and later to perform some tasks when we’ll add an action button to the notification. Also, don’t forget to add this class inside the manifest file.
<?xml version="1.0" encoding="utf-8"?> | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
package="com.example.todolist"> | |
<application | |
..... | |
<receiver android:name=".presentation.br.AlarmReceiver"/> | |
..... | |
</application> | |
</manifest> |
Step 3: Set the alarm
We will be using the setAlarmClock() method to set an alarm since I found it to be the most reliable when setting multiple alarms in the future. There are multiple methods to set an alarm as well such as —
set()
setExact()
setInExactRepeating()
setRepeating()
setTime()
setTimeZone()
setAndAllowWhileIdle()
setExactAndAllowWhileIdle()
setWindow()
In the setAlarm(taskInfo: TaskInfo) function we first create an instance of alarm manager and then set a pending intent to open the AlarmReciver class in the future which will then fire the notification. Notice that we have used taskInfo.id as the requestCode which will make sure that we have different requestCode for each alarm and thus can create multiple alarms with multiple notifications.
Some things about the setAlarmClock() –
Using this method the alarm clock icon will appear in the user’s status bar as if they had set an alarm with their device’s built-in alarm clock app and when the user fully slides open their notification, they will see the time of the alarm. Tapping on the alarm time in the notification will activate the PendingIntent we specified in the AlarmClockInfo object that is basicPendingIntent here.
private fun setAlarm(taskInfo: TaskInfo) { | |
// creating alarmManager instance | |
val alarmManager = activity?.getSystemService(Context.ALARM_SERVICE) as AlarmManager | |
// adding intent and pending intent to go to AlarmReceiver Class in future | |
val intent = Intent(requireContext(), AlarmReceiver::class.java) | |
intent.putExtra("task_info", taskInfo) | |
val pendingIntent = PendingIntent.getBroadcast(requireContext(), taskInfo.id, intent, PendingIntent.FLAG_IMMUTABLE) | |
// when using setAlarmClock() it displays a notification until alarm rings and when pressed it takes us to mainActivity | |
val mainActivityIntent = Intent(requireContext(), MainActivity::class.java) | |
val basicPendingIntent = PendingIntent.getActivity(requireContext(), taskInfo.id, mainActivityIntent, PendingIntent.FLAG_IMMUTABLE) | |
// creating clockInfo instance | |
val clockInfo = AlarmManager.AlarmClockInfo(taskInfo.date.time, basicPendingIntent) | |
// setting the alarm | |
alarmManager.setAlarmClock(clockInfo, pendingIntent) | |
} |
While setting any kind of exact alarm we need to add the SCHEDULE_EXACT_ALARM permission in the manifest file for the latest APIs.
<manifest ...> | |
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> | |
<application ...> | |
... | |
</application> | |
</manifest> |
Step 4: Create the notification channel
Starting in Android 8.0 (API level 26), all notifications must be assigned to a channel. For example, here we can create three channels for different task priorities set by users such as low, medium, and high each with a different visual and audio. NotificationChannel takes 3 parameters (channel_id, channel_name, priority). channel_id must be different for each channel and channel_name should describe basic info about the channel which can be viewed by the user inside the App Info. It only needs to be set up once and can be called inside the onCreate() method.
private fun createNotificationChannel() { | |
val importance = NotificationManager.IMPORTANCE_HIGH | |
val channel = NotificationChannel("to_do_list", "Tasks Notification Channel", importance).apply { | |
description = "Notification for Tasks" | |
} | |
val notificationManager = activity?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | |
notificationManager.createNotificationChannel(channel) | |
} |
Job Offers
Step 5: Cancelling the alarm(Optional)
Let’s imagine we have a scheduled alarm for a task that the user wants to complete however it turned out the user has completed it way before that. In such a case we need to cancel the alarm. It is very easy to cancel the alarm, however, one must make sure that the pendingIntent used to cancel it must match the pendingIntent we used to set the alarm, or else it won’t cancel. It is not necessary to put the same content inside the intent we used previously because the contents inside the intent are not matched when comparing pending intents. (such as intent.putExtra)
private fun removeAlarm(taskInfo: TaskInfo){ | |
val alarmManager = activity?.getSystemService(Context.ALARM_SERVICE) as AlarmManager | |
val intent = Intent(requireContext(), AlarmReceiver::class.java) | |
// It is not necessary to add putExtra | |
intent.putExtra("task_info", taskInfo) | |
val pendingIntent = PendingIntent.getBroadcast(requireContext(), taskInfo.id, intent, PendingIntent.FLAG_IMMUTABLE) | |
alarmManager.cancel(pendingIntent) | |
} |
Step 6: Adding alarms when the device restarts
When a device turns off, all alarms are canceled by default. To avoid this, we must design our application to automatically reset the alarm if the user reboots the device. This assures that the AlarmManager will continue to function without the user having to restart the alarm. We must add a broadcast receiver class that receives a broadcast whenever the device restarts.
@AndroidEntryPoint | |
class RebootBroadcastReceiver : BroadcastReceiver(){ | |
@Inject | |
lateinit var repository: TaskCategoryRepositoryImpl | |
override fun onReceive(context: Context?, p1: Intent?) { | |
val time = Date() | |
CoroutineScope(Main).launch { | |
val list = repository.getActiveAlarms(time) | |
for(taskInfo in list) setAlarm(taskInfo, context) | |
} | |
} | |
private fun setAlarm(taskInfo: TaskInfo, context: Context?){ | |
val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager | |
val intent = Intent(context, AlarmReceiver::class.java) | |
intent.putExtra("task_info", taskInfo) | |
val pendingIntent = PendingIntent.getBroadcast(context, taskInfo.id, intent, PendingIntent.FLAG_IMMUTABLE) | |
val mainActivityIntent = Intent(context, MainActivity::class.java) | |
val basicPendingIntent = PendingIntent.getActivity(context, taskInfo.id, mainActivityIntent, PendingIntent.FLAG_IMMUTABLE) | |
val clockInfo = AlarmManager.AlarmClockInfo(taskInfo.date.time, basicPendingIntent) | |
alarmManager.setAlarmClock(clockInfo, pendingIntent) | |
} | |
} |
Here we are getting the list of all active alarms using our repository and setting the alarms back inside this class. Also, we need to add RECEIVE_BOOT_COMPLETED permission in the Android Manifest file along with the RebootBroadcastReceiver class and its intent filters. These intent filters enable it to receive the broadcast when either the device is restarted or switched on.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
...> | |
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | |
<application | |
... | |
<receiver android:name=".presentation.br.RebootBroadcastReceiver" | |
android:enabled="true" | |
android:exported="true"> | |
<intent-filter> | |
<action android:name="android.intent.action.BOOT_COMPLETED" /> | |
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> | |
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> | |
</intent-filter> | |
</receiver> | |
... | |
</application> | |
</manifest> |
Note — In newer mobile phones you need to manually switch on the auto-launch feature inside App Info → Battery Management → AutoLaunch in order for it to receive the RECEIVE_BOOT_COMPLETED broadcast. It is turned off initially for battery management purposes. Basically it enables your application to auto launch whenever it receives the broadcast.
Step 7: Adding action button (Optional)
Let’s say a user receives the notification for a task and has completed it, in that case, the user would like to press a finish/completed button so that it can be removed from the queue of uncompleted tasks. Now we’ll be adding an action button to complete a task. Here we will not be opening any activity when the user taps the button but performing some work in the background. For this, we need to add some code inside the AlarmReceiver class we created earlier and add another broadcast receiver class that will receive the broadcast to perform some action when a task is completed. Inside AlarmReceiver class we need to add one more pendingIntent that will be responsible to open OnCompletedBroadcastReceiver class.
@AndroidEntryPoint | |
class OnCompletedBroadcastReceiver : BroadcastReceiver() { | |
@Inject lateinit var repository: TaskCategoryRepositoryImpl | |
override fun onReceive(p0: Context?, p1: Intent?) { | |
val taskInfo = p1?.getSerializableExtra("task_info") as? TaskInfo | |
if (taskInfo != null) { | |
taskInfo.status = true | |
} | |
CoroutineScope(IO).launch { | |
taskInfo?.let { | |
repository.updateTaskStatus(it) | |
} | |
} | |
if (p0 != null && taskInfo != null) { | |
// used to remove the notification when a user taps completed button (Optional) | |
NotificationManagerCompat.from(p0).cancel(null, taskInfo.id) | |
} | |
} | |
} |
This class receives the taskInfo object when the user taps on the action button and changes its status to true since the task is completed and updates it inside the Room Database using the repository.
Also, add this class in the AndroidManifest.xml file.
<?xml version="1.0" encoding="utf-8"?> | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
package="com.example.todolist"> | |
<application | |
..... | |
<receiver android:name=".presentation.br.OnCompletedBroadcastReceiver"/> | |
..... | |
</application> | |
</manifest> |
class AlarmReceiver : BroadcastReceiver() { | |
private var notificationManager: NotificationManagerCompat? = null | |
override fun onReceive(p0: Context?, p1: Intent?) { | |
val taskInfo = p1?.getSerializableExtra("task_info") as? TaskInfo | |
val tapResultIntent = Intent(p0, MainActivity::class.java) | |
tapResultIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP | |
val pendingIntent: PendingIntent = getActivity( p0,0,tapResultIntent,FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) | |
// creating intent and pending intent to open OnCompletedBroadcastReceiver class | |
val intentOnCompleted = Intent(p0, OnCompletedBroadcastReceiver::class.java).apply { | |
putExtra("task_info", taskInfo) | |
} | |
val pendingIntentOnCompleted: PendingIntent? = | |
taskInfo?.let { getBroadcast(p0, it.id,intentOnCompleted,FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } | |
// creating the action button | |
val actionCompleted : NotificationCompat.Action = NotificationCompat.Action.Builder(0,"Completed",pendingIntentOnCompleted).build() | |
val notification = p0?.let { | |
NotificationCompat.Builder(it, "to_do_list") | |
.setContentTitle("Task Reminder") | |
.setContentText(taskInfo?.description) | |
.setSmallIcon(R.mipmap.ic_launcher) | |
.setAutoCancel(true) | |
.setPriority(NotificationCompat.PRIORITY_HIGH) | |
.setContentIntent(pendingIntent) | |
.addAction(actionCompleted) // adding the action button to notification | |
.build() | |
} | |
notificationManager = p0?.let { NotificationManagerCompat.from(it) } | |
notification?.let { taskInfo?.let { it1 -> notificationManager?.notify(it1.id, it) } } | |
} | |
} |
There are a few other things we can do with notifications, like adding a reply button and retrieving the notification’s user input text, but those are for another time.
Suggestions and feedback are appreciated. I hope this blog was useful to you! See you later and thank you for taking the time to read this.
You can find the link to the repository here and you can connect with me on GitHub and LinkedIn.
Thanks to Mario Sanoguera de Lorenzo
This article was originally published on proandroiddev.com on September 26, 2022