Browse Source

fix(test-app): register NotifyReceiver in AndroidManifest

The Vue test app was missing the NotifyReceiver registration in
AndroidManifest.xml, preventing alarm broadcasts from being delivered
to the BroadcastReceiver. This caused notifications scheduled via
setAlarmClock() to fire but not display.

Added NotifyReceiver registration matching the working android-test-app
configuration. Also includes supporting improvements:
- Enhanced alarm scheduling with setAlarmClock() for Doze exemption
- Unique request codes based on trigger time to prevent PendingIntent conflicts
- Diagnostic methods (isAlarmScheduled, getNextAlarmTime, testAlarm)
- TypeScript definitions for new methods

Verified: Notification successfully fired at 09:41:00 and was displayed.
master
Matthew Raymer 7 days ago
parent
commit
a19cb2ba61
  1. 79
      android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt
  2. 181
      android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt
  3. 22
      src/definitions.ts
  4. 7
      test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml
  5. 1
      test-apps/daily-notification-test/android/capacitor.settings.gradle

79
android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt

@ -866,6 +866,85 @@ open class DailyNotificationPlugin : Plugin() {
}
}
/**
* Check if an alarm is scheduled for a given trigger time
*/
@PluginMethod
fun isAlarmScheduled(call: PluginCall) {
try {
val options = call.data ?: return call.reject("Options are required")
val triggerAtMillis = options.getLong("triggerAtMillis") ?: return call.reject("triggerAtMillis is required")
val context = context ?: return call.reject("Context not available")
val isScheduled = NotifyReceiver.isAlarmScheduled(context, triggerAtMillis)
val result = JSObject().apply {
put("scheduled", isScheduled)
put("triggerAtMillis", triggerAtMillis)
}
Log.i(TAG, "Checking alarm status: scheduled=$isScheduled, triggerAt=$triggerAtMillis")
call.resolve(result)
} catch (e: Exception) {
Log.e(TAG, "Failed to check alarm status", e)
call.reject("Failed to check alarm status: ${e.message}")
}
}
/**
* Get the next scheduled alarm time from AlarmManager
*/
@PluginMethod
fun getNextAlarmTime(call: PluginCall) {
try {
val context = context ?: return call.reject("Context not available")
val nextAlarmTime = NotifyReceiver.getNextAlarmTime(context)
val result = JSObject().apply {
if (nextAlarmTime != null) {
put("scheduled", true)
put("triggerAtMillis", nextAlarmTime)
} else {
put("scheduled", false)
}
}
Log.i(TAG, "Getting next alarm time: ${if (nextAlarmTime != null) nextAlarmTime else "none"}")
call.resolve(result)
} catch (e: Exception) {
Log.e(TAG, "Failed to get next alarm time", e)
call.reject("Failed to get next alarm time: ${e.message}")
}
}
/**
* Test method: Schedule an alarm to fire in a few seconds
* Useful for verifying alarm delivery works correctly
*/
@PluginMethod
fun testAlarm(call: PluginCall) {
try {
val options = call.data
val secondsFromNow = options?.getInt("secondsFromNow") ?: 5
val context = context ?: return call.reject("Context not available")
Log.i(TAG, "TEST: Scheduling test alarm in $secondsFromNow seconds")
NotifyReceiver.testAlarm(context, secondsFromNow)
val result = JSObject().apply {
put("scheduled", true)
put("secondsFromNow", secondsFromNow)
put("triggerAtMillis", System.currentTimeMillis() + (secondsFromNow * 1000L))
}
call.resolve(result)
} catch (e: Exception) {
Log.e(TAG, "Failed to schedule test alarm", e)
call.reject("Failed to schedule test alarm: ${e.message}")
}
}
@PluginMethod
fun scheduleUserNotification(call: PluginCall) {
try {

181
android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt

@ -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 {

22
src/definitions.ts

@ -605,6 +605,28 @@ export interface DailyNotificationPlugin {
// Existing methods
scheduleDailyNotification(options: NotificationOptions): Promise<void>;
/**
* Check if an alarm is scheduled for a given trigger time
* @param options Object containing triggerAtMillis (number)
* @returns Object with scheduled (boolean) and triggerAtMillis (number)
*/
isAlarmScheduled(options: { triggerAtMillis: number }): Promise<{ scheduled: boolean; triggerAtMillis: number }>;
/**
* Get the next scheduled alarm time from AlarmManager
* @returns Object with scheduled (boolean) and triggerAtMillis (number | null)
*/
getNextAlarmTime(): Promise<{ scheduled: boolean; triggerAtMillis?: number }>;
/**
* Test method: Schedule an alarm to fire in a few seconds
* Useful for verifying alarm delivery works correctly
* @param options Object containing secondsFromNow (number, default: 5)
* @returns Object with scheduled (boolean), secondsFromNow (number), and triggerAtMillis (number)
*/
testAlarm(options?: { secondsFromNow?: number }): Promise<{ scheduled: boolean; secondsFromNow: number; triggerAtMillis: number }>;
getLastNotification(): Promise<NotificationResponse | null>;
cancelAllNotifications(): Promise<void>;
getNotificationStatus(): Promise<NotificationStatus>;

7
test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml

@ -36,6 +36,13 @@
</intent-filter>
</receiver>
<!-- NotifyReceiver for AlarmManager-based notifications -->
<receiver
android:name="com.timesafari.dailynotification.NotifyReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"

1
test-apps/daily-notification-test/android/capacitor.settings.gradle

@ -3,5 +3,4 @@ include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':timesafari-daily-notification-plugin'
// Plugin now uses standard structure: android/ (not android/plugin/)
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')

Loading…
Cancel
Save