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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user