@ -1,6 +1,7 @@
package com.timesafari.dailynotification
import android.app.AlarmManager
import android.app.AlarmManager.AlarmClockInfo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
@ -27,8 +28,26 @@ class NotifyReceiver : BroadcastReceiver() {
private const val TAG = " DNP-NOTIFY "
private const val CHANNEL_ID = " daily_notifications "
private const val NOTIFICATION_ID = 1001
private const val REQUEST_CODE = 2001
/ * *
* Generate unique request code from trigger time
* Uses lower 16 bits of timestamp to ensure uniqueness
* /
private fun getRequestCode ( triggerAtMillis : Long ) : Int {
return ( triggerAtMillis and 0xFFFF ) . toInt ( )
}
/ * *
* Schedule an exact notification using AlarmManager
* Uses setAlarmClock ( ) for Android 5.0 + for better reliability
* Falls back to setExactAndAllowWhileIdle for older versions
*
* @param context Application context
* @param triggerAtMillis When to trigger the notification ( UTC milliseconds )
* @param config Notification configuration
* @param isStaticReminder Whether this is a static reminder ( no content dependency )
* @param reminderId Optional reminder ID for tracking
* /
fun scheduleExactNotification (
context : Context ,
triggerAtMillis : Long ,
@ -44,33 +63,75 @@ class NotifyReceiver : BroadcastReceiver() {
putExtra ( " vibration " , config . vibration ?: true )
putExtra ( " priority " , config . priority ?: " normal " )
putExtra ( " is_static_reminder " , isStaticReminder )
putExtra ( " trigger_time " , triggerAtMillis ) // Store trigger time for debugging
if ( reminderId != null ) {
putExtra ( " reminder_id " , reminderId )
}
}
// Use unique request code based on trigger time to prevent PendingIntent conflicts
val requestCode = getRequestCode ( triggerAtMillis )
val pendingIntent = PendingIntent . getBroadcast (
context ,
REQUEST_CODE ,
requestCode ,
intent ,
PendingIntent . FLAG_UPDATE_CURRENT or PendingIntent . FLAG_IMMUTABLE
)
val currentTime = System . currentTimeMillis ( )
val delayMs = triggerAtMillis - currentTime
val triggerTimeStr = java . text . SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " , java . util . Locale . US )
. format ( java . util . Date ( triggerAtMillis ) )
Log . i ( TAG , " Scheduling alarm: triggerTime= $triggerTimeStr , delayMs= $delayMs , requestCode= $requestCode " )
try {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M ) {
// Use setAlarmClock() for Android 5.0+ (API 21+) - most reliable method
// Shows alarm icon in status bar and is exempt from doze mode
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
// Create show intent for alarm clock (opens app when alarm fires)
val showIntent = try {
Intent ( context , Class . forName ( " com.timesafari.dailynotification.MainActivity " ) ) . apply {
flags = Intent . FLAG_ACTIVITY_NEW_TASK or Intent . FLAG_ACTIVITY_CLEAR_TASK
}
} catch ( e : ClassNotFoundException ) {
Log . w ( TAG , " MainActivity not found, using package launcher " , e )
context . packageManager . getLaunchIntentForPackage ( context . packageName ) ?. apply {
flags = Intent . FLAG_ACTIVITY_NEW_TASK or Intent . FLAG_ACTIVITY_CLEAR_TASK
}
}
val showPendingIntent = if ( showIntent != null ) {
PendingIntent . getActivity (
context ,
requestCode + 1 , // Different request code for show intent
showIntent ,
PendingIntent . FLAG_UPDATE_CURRENT or PendingIntent . FLAG_IMMUTABLE
)
} else {
null
}
val alarmClockInfo = AlarmClockInfo ( triggerAtMillis , showPendingIntent )
alarmManager . setAlarmClock ( alarmClockInfo , pendingIntent )
Log . i ( TAG , " Alarm clock scheduled (setAlarmClock): triggerAt= $triggerAtMillis , requestCode= $requestCode " )
} else if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M ) {
// Fallback to setExactAndAllowWhileIdle for Android 6.0-4.4
alarmManager . setExactAndAllowWhileIdle (
AlarmManager . RTC_WAKEUP ,
triggerAtMillis ,
pendingIntent
)
Log . i ( TAG , " Exact alarm scheduled (setExactAndAllowWhileIdle): triggerAt= $triggerAtMillis , requestCode= $requestCode " )
} else {
// Fallback to setExact for older versions
alarmManager . setExact (
AlarmManager . RTC_WAKEUP ,
triggerAtMillis ,
pendingIntent
)
Log . i ( TAG , " Exact alarm scheduled (setExact): triggerAt= $triggerAtMillis , requestCode= $requestCode " )
}
Log . i ( TAG , " Exact notification scheduled for: $triggerAtMillis " )
} catch ( e : SecurityException ) {
Log . w ( TAG , " Cannot schedule exact alarm, falling back to inexact " , e )
alarmManager . set (
@ -78,25 +139,129 @@ class NotifyReceiver : BroadcastReceiver() {
triggerAtMillis ,
pendingIntent
)
Log . i ( TAG , " Inexact alarm scheduled (fallback): triggerAt= $triggerAtMillis , requestCode= $requestCode " )
}
}
fun cancelNotification ( context : Context ) {
/ * *
* Cancel a scheduled notification alarm
* @param context Application context
* @param triggerAtMillis The trigger time of the alarm to cancel ( required for unique request code )
* /
fun cancelNotification ( context : Context , triggerAtMillis : Long ) {
val alarmManager = context . getSystemService ( Context . ALARM_SERVICE ) as AlarmManager
val intent = Intent ( context , NotifyReceiver :: class . java )
val requestCode = getRequestCode ( triggerAtMillis )
val pendingIntent = PendingIntent . getBroadcast (
context ,
REQUEST_CODE ,
requestCode ,
intent ,
PendingIntent . FLAG_UPDATE_CURRENT or PendingIntent . FLAG_IMMUTABLE
)
alarmManager . cancel ( pendingIntent )
Log . i ( TAG , " Notification alarm cancelled " )
Log . i ( TAG , " Notification alarm cancelled: triggerAt= $triggerAtMillis , requestCode= $requestCode " )
}
/ * *
* Check if an alarm is scheduled for the given trigger time
* @param context Application context
* @param triggerAtMillis The trigger time to check
* @return true if alarm is scheduled , false otherwise
* /
fun isAlarmScheduled ( context : Context , triggerAtMillis : Long ) : Boolean {
val intent = Intent ( context , NotifyReceiver :: class . java )
val requestCode = getRequestCode ( triggerAtMillis )
val pendingIntent = PendingIntent . getBroadcast (
context ,
requestCode ,
intent ,
PendingIntent . FLAG_NO_CREATE or PendingIntent . FLAG_IMMUTABLE
)
val isScheduled = pendingIntent != null
val triggerTimeStr = java . text . SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " , java . util . Locale . US )
. format ( java . util . Date ( triggerAtMillis ) )
Log . d ( TAG , " Alarm check for $triggerTimeStr : scheduled= $isScheduled , requestCode= $requestCode " )
return isScheduled
}
/ * *
* Get the next scheduled alarm time from AlarmManager
* @param context Application context
* @return Next alarm time in milliseconds , or null if no alarm is scheduled
* /
fun getNextAlarmTime ( context : Context ) : Long ? {
return try {
val alarmManager = context . getSystemService ( Context . ALARM_SERVICE ) as AlarmManager
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
val nextAlarm = alarmManager . nextAlarmClock
if ( nextAlarm != null ) {
val triggerTime = nextAlarm . triggerTime
val triggerTimeStr = java . text . SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " , java . util . Locale . US )
. format ( java . util . Date ( triggerTime ) )
Log . d ( TAG , " Next alarm clock: $triggerTimeStr " )
triggerTime
} else {
Log . d ( TAG , " No alarm clock scheduled " )
null
}
} else {
Log . d ( TAG , " getNextAlarmTime() requires Android 5.0+ " )
null
}
} catch ( e : Exception ) {
Log . e ( TAG , " Error getting next alarm time " , e )
null
}
}
/ * *
* Test method : Schedule an alarm to fire in a few seconds
* Useful for verifying alarm delivery works correctly
* @param context Application context
* @param secondsFromNow How many seconds from now to fire ( default : 5 )
* /
fun testAlarm ( context : Context , secondsFromNow : Int = 5 ) {
val triggerTime = System . currentTimeMillis ( ) + ( secondsFromNow * 1000L )
val config = UserNotificationConfig (
enabled = true ,
schedule = " " ,
title = " Test Notification " ,
body = " This is a test notification scheduled $secondsFromNow seconds from now " ,
sound = true ,
vibration = true ,
priority = " high "
)
val triggerTimeStr = java . text . SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " , java . util . Locale . US )
. format ( java . util . Date ( triggerTime ) )
Log . i ( TAG , " TEST: Scheduling test alarm for $triggerTimeStr (in $secondsFromNow seconds) " )
scheduleExactNotification (
context ,
triggerTime ,
config ,
isStaticReminder = true ,
reminderId = " test_ ${System.currentTimeMillis()} "
)
Log . i ( TAG , " TEST: Alarm scheduled. Check logs in $secondsFromNow seconds for 'Notification receiver triggered' " )
}
}
override fun onReceive ( context : Context , intent : Intent ? ) {
Log . i ( TAG , " Notification receiver triggered " )
val triggerTime = intent ?. getLongExtra ( " trigger_time " , 0L ) ?: 0L
val triggerTimeStr = if ( triggerTime > 0 ) {
java . text . SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " , java . util . Locale . US )
. format ( java . util . Date ( triggerTime ) )
} else {
" unknown "
}
val currentTime = System . currentTimeMillis ( )
val delayMs = if ( triggerTime > 0 ) currentTime - triggerTime else 0L
Log . i ( TAG , " Notification receiver triggered: triggerTime= $triggerTimeStr , currentTime= ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).format(java.util.Date(currentTime))} , delayMs= $delayMs " )
CoroutineScope ( Dispatchers . IO ) . launch {
try {