Prologue
As of API 19, all AlarmManager iterators are incorrect. This creates many difficulties for programmers. After a while of researching, I decided to use setExact to send notifications once. In the notification function, I will use the data in the room database to set it up for the next notification.
1. Granting permissions
- We need the SCHEDULE_EXACT_ALARM permission for the AlarmManager.setExact function to work.
- SCHEDULE_EXACT_ALARM will be automatically granted for API <= 31
- For API > 31 we need to check and ask for permission if not allowed.
- Check permissions:
1 2 | ContextCompat.checkSelfPermission(context, Manifest.permission.SCHEDULE_EXACT_ALARM) |
Request permission:
1 2 3 4 5 6 7 8 | val launcher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { // TODO } launcher.launch(Manifest.permission.SCHEDULE_EXACT_ALARM) |
2. Create ReminderBroadcastReceiver
- We use BroadcastReceiver to receive event and display Notification to user
- Use room to test and execute setExact Alarm for next time
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | @AndroidEntryPoint class ReminderBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var reminderRepository: ReminderRepository companion object { const val CONTENT_TEXT = "CONTENT_TEXT" const val REMINDER_ID = "REMINDER_ID" } override fun onReceive(context: Context?, intent: Intent?) { val ctx = context ?: return val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (!notificationManager.areNotificationsEnabled()) return val reminderId = intent?.getIntExtra(REMINDER_ID, -1) ?: -1 if (reminderId != -1) { CoroutineScope(Dispatchers.IO).launch { reminderRepository.checkToResetReminder(reminderId) } } val notification = NotificationCompat.Builder(ctx, Constants.REMINDER_CHANNEL_ID) .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle(ctx.getString(R.string.app_name)) .setContentText(intent?.getStringExtra(CONTENT_TEXT)) .build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( Constants.REMINDER_CHANNEL_ID, Constants.REMINDER_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT ).apply { description = Constants.REMINDER_CHANNEL_DESC } notificationManager.createNotificationChannel(channel) } notificationManager.notify(Random.nextInt(), notification) } } |
- If the reminder in the room has not been deleted, then set it for the next time
1 2 3 4 5 6 7 8 9 10 11 12 | override suspend fun checkToResetReminder(id: Int) { try { val reminder = getReminderById(id.toLong()) reminderManager.scheduleReminder( content = reminder.label, time = reminder.reminderTime, reminderId = reminder.id.toInt() ) } catch (_: Exception) { } } |
Add to manifest file
1 2 3 4 | <receiver android:name=".ui.reminder_editing.ReminderBroadcastReceiver" android:enabled="true" /> |
3. Create a class to manage adding and canceling Notifications
- Here I use requestCode in pendingIntent equal to the Id of the notification in the room for the convenience of canceling Notification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class ReminderManager( private val context: Context ) { fun scheduleReminder(content: String, time: LocalTime, reminderId: Int) { val intent = Intent(context, ReminderBroadcastReceiver::class.java) intent.putExtra(ReminderBroadcastReceiver.CONTENT_TEXT, content) intent.putExtra(ReminderBroadcastReceiver.REMINDER_ID, reminderId) val pendingIntent = PendingIntent.getBroadcast( context, reminderId, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val notificationTime = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, time.hour) set(Calendar.MINUTE, time.minute) set(Calendar.SECOND, 0) } // Do not trigger reminder in the pass if (Calendar.getInstance().apply { add(Calendar.SECOND, 30) }.timeInMillis - notificationTime.timeInMillis > 0) { notificationTime.add(Calendar.DATE, 1) } val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmManager.setExact( AlarmManager.RTC_WAKEUP, notificationTime.timeInMillis, pendingIntent ) } fun cancelReminder(reminderId: Int) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, ReminderBroadcastReceiver::class.java) val pendingIntent = PendingIntent.getBroadcast( context, reminderId, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) alarmManager.cancel(pendingIntent) } } |
4. Call the function that displays Notification
- Add Notification in Room Database.
- Use the returned Id to set the notificationId to facilitate editing and canceling Notification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Entity(tableName = TABLE_REMINDER) data class Reminder constructor( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, @ColumnInfo("label") val label: String, @ColumnInfo("activated") val activated: Boolean, @ColumnInfo("color") val color: Long, @ColumnInfo("reminder_time") val reminderTime: LocalTime, @ColumnInfo("id_user") val idUser: String? = null ) @Dao interface ReminderDao { @Query("SELECT * FROM ${Constants.TABLE_REMINDER}") fun getReminderList(): Flow<List<Reminder>> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun addReminder(reminder: Reminder): Long // NotificationId @Query("SELECT * FROM ${Constants.TABLE_REMINDER} WHERE id=:id") suspend fun getReminder(id: Long): Reminder @Query("DELETE FROM ${Constants.TABLE_REMINDER} WHERE id=:id") suspend fun deleteReminder(id: Long) } |
- Trigger
1 2 3 | val reminderManager = ReminderManager(context); reminderManager.scheduleReminder("Hello!", LocalTime(8, 0), NotificationId); |
- Reboot handling
- After the phone is rebooted. The functions in the AlarmManager will be reset. So we need to catch reboot event and reset reminder to show Notification. Here I will use broadcast receiver
1 2 3 4 5 6 7 8 9 | class BootReceiver : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { if (p1?.action == "android.intent.action.BOOT_COMPLETED") { // Đặt lại Notification ở đây, dựa vào dữ liệu được lưu trữ trong room } } } |
- Declare in manifest
1 2 3 4 5 6 7 8 9 10 | <receiver android:name=".ui.reminder_editing.BootReceiver" android:enabled="true" android:exported="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> |