Compare commits
4 Commits
android-6-
...
rollover-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2714480070 | ||
|
|
e873a46bbd | ||
|
|
aa0eaa5389 | ||
|
|
c36781e440 |
@@ -35,7 +35,7 @@ import org.json.JSONObject
|
|||||||
* Bridges Capacitor calls to native Android functionality
|
* Bridges Capacitor calls to native Android functionality
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.1.0
|
* @version 1.3.0
|
||||||
*/
|
*/
|
||||||
@CapacitorPlugin(name = "DailyNotification")
|
@CapacitorPlugin(name = "DailyNotification")
|
||||||
open class DailyNotificationPlugin : Plugin() {
|
open class DailyNotificationPlugin : Plugin() {
|
||||||
@@ -1109,8 +1109,13 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
val sound = options.getBoolean("sound") ?: true
|
val sound = options.getBoolean("sound") ?: true
|
||||||
val priority = options.getString("priority") ?: "default"
|
val priority = options.getString("priority") ?: "default"
|
||||||
val url = options.getString("url") // Optional URL for prefetch (not used in helper yet)
|
val url = options.getString("url") // Optional URL for prefetch (not used in helper yet)
|
||||||
|
val rolloverIntervalMinutes = try {
|
||||||
|
(options.getInt("rolloverIntervalMinutes") ?: 0).takeIf { it > 0 }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Scheduling daily notification: time=$time, title=$title")
|
Log.i(TAG, "Scheduling daily notification: time=$time, title=$title, rolloverIntervalMinutes=$rolloverIntervalMinutes")
|
||||||
|
|
||||||
// Convert HH:mm time to cron expression (daily at specified time)
|
// Convert HH:mm time to cron expression (daily at specified time)
|
||||||
val cronExpression = convertTimeToCron(time)
|
val cronExpression = convertTimeToCron(time)
|
||||||
@@ -1137,6 +1142,14 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
Log.i(TAG, "scheduleDailyNotification: No cleanup needed - existing schedule will be updated via upsert: $scheduleId")
|
Log.i(TAG, "scheduleDailyNotification: No cleanup needed - existing schedule will be updated via upsert: $scheduleId")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel only fetch-related WorkManager jobs so they cannot create a second (UUID) alarm
|
||||||
|
// with fallback or placeholder text. Does not cancel display/dismiss; future fetched-content
|
||||||
|
// flows should use distinct tags so they are not affected.
|
||||||
|
val workCancelled = ScheduleHelper.cancelFetchRelatedWorkManagerJobs(context)
|
||||||
|
if (workCancelled) {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Cancelled pending prefetch/fetch WorkManager jobs")
|
||||||
|
}
|
||||||
|
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
schedule = cronExpression,
|
schedule = cronExpression,
|
||||||
@@ -1154,7 +1167,8 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
scheduleId,
|
scheduleId,
|
||||||
config,
|
config,
|
||||||
time,
|
time,
|
||||||
::calculateNextRunTime
|
::calculateNextRunTime,
|
||||||
|
rolloverIntervalMinutes
|
||||||
)
|
)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -2650,6 +2664,7 @@ object ScheduleHelper {
|
|||||||
* @param config User notification configuration
|
* @param config User notification configuration
|
||||||
* @param clockTime Original HH:mm time string
|
* @param clockTime Original HH:mm time string
|
||||||
* @param calculateNextRunTime Function to calculate next run time from cron expression
|
* @param calculateNextRunTime Function to calculate next run time from cron expression
|
||||||
|
* @param rolloverIntervalMinutes When > 0, next occurrence is this many minutes after trigger (dev/testing). Null/0 = 24h.
|
||||||
* @return true if successful, false otherwise
|
* @return true if successful, false otherwise
|
||||||
*/
|
*/
|
||||||
suspend fun scheduleDailyNotification(
|
suspend fun scheduleDailyNotification(
|
||||||
@@ -2658,7 +2673,8 @@ object ScheduleHelper {
|
|||||||
scheduleId: String,
|
scheduleId: String,
|
||||||
config: UserNotificationConfig,
|
config: UserNotificationConfig,
|
||||||
clockTime: String,
|
clockTime: String,
|
||||||
calculateNextRunTime: (String) -> Long
|
calculateNextRunTime: (String) -> Long,
|
||||||
|
rolloverIntervalMinutes: Int? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val nextRunTime = calculateNextRunTime(config.schedule)
|
val nextRunTime = calculateNextRunTime(config.schedule)
|
||||||
@@ -2689,14 +2705,15 @@ object ScheduleHelper {
|
|||||||
// schedule a second alarm via legacy DailyNotificationScheduler, resulting in duplicate
|
// schedule a second alarm via legacy DailyNotificationScheduler, resulting in duplicate
|
||||||
// notifications at fire time.
|
// notifications at fire time.
|
||||||
|
|
||||||
// Store schedule in database
|
// Store schedule in database (include rollover interval for dev/testing; survives reboot)
|
||||||
val schedule = Schedule(
|
val schedule = Schedule(
|
||||||
id = scheduleId,
|
id = scheduleId,
|
||||||
kind = "notify",
|
kind = "notify",
|
||||||
cron = config.schedule,
|
cron = config.schedule,
|
||||||
clockTime = clockTime,
|
clockTime = clockTime,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
nextRunAt = nextRunTime
|
nextRunAt = nextRunTime,
|
||||||
|
rolloverIntervalMinutes = rolloverIntervalMinutes
|
||||||
)
|
)
|
||||||
database.scheduleDao().upsert(schedule)
|
database.scheduleDao().upsert(schedule)
|
||||||
|
|
||||||
@@ -2705,7 +2722,7 @@ object ScheduleHelper {
|
|||||||
try {
|
try {
|
||||||
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
scheduleId,
|
scheduleId,
|
||||||
"1.2.0",
|
"1.3.0",
|
||||||
null,
|
null,
|
||||||
"daily",
|
"daily",
|
||||||
config.title ?: "Daily Notification",
|
config.title ?: "Daily Notification",
|
||||||
@@ -2734,6 +2751,35 @@ object ScheduleHelper {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking load of a schedule by id (for use from Java Worker / rollover path).
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getScheduleBlocking(context: Context, scheduleId: String): Schedule? {
|
||||||
|
return kotlinx.coroutines.runBlocking {
|
||||||
|
try {
|
||||||
|
DailyNotificationDatabase.getDatabase(context).scheduleDao().getById(scheduleId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("ScheduleHelper", "getScheduleBlocking failed: $scheduleId", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking update of schedule next run time (for use from Java Worker after rollover).
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun updateScheduleNextRunTimeBlocking(context: Context, scheduleId: String, lastRunAt: Long?, nextRunAt: Long) {
|
||||||
|
kotlinx.coroutines.runBlocking {
|
||||||
|
try {
|
||||||
|
DailyNotificationDatabase.getDatabase(context).scheduleDao().updateRunTimes(scheduleId, lastRunAt, nextRunAt)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("ScheduleHelper", "updateScheduleNextRunTimeBlocking failed: $scheduleId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule dual notification (fetch + notify)
|
* Schedule dual notification (fetch + notify)
|
||||||
@@ -2846,9 +2892,30 @@ object ScheduleHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel only WorkManager jobs that can create a second (UUID) alarm for the static-reminder path:
|
||||||
|
* prefetch and daily_notification_fetch. Does not cancel display, dismiss, or maintenance.
|
||||||
|
* Use this when handling scheduleDailyNotification so pending prefetch does not run and create
|
||||||
|
* a duplicate alarm; future fetched-content flows should use distinct tags so they are not affected.
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @return true if cancellation was successful
|
||||||
|
*/
|
||||||
|
suspend fun cancelFetchRelatedWorkManagerJobs(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
|
workManager.cancelAllWorkByTag("prefetch")
|
||||||
|
workManager.cancelAllWorkByTag("daily_notification_fetch")
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("ScheduleHelper", "Failed to cancel fetch-related WorkManager jobs", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel all WorkManager jobs by tags
|
* Cancel all WorkManager jobs by tags
|
||||||
*
|
*
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @return true if cancellation was successful
|
* @return true if cancellation was successful
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -535,8 +535,33 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
try {
|
try {
|
||||||
Log.d(TAG, "DN|RESCHEDULE_START id=" + content.getId());
|
Log.d(TAG, "DN|RESCHEDULE_START id=" + content.getId());
|
||||||
|
|
||||||
// Calculate next occurrence using DST-safe ZonedDateTime
|
// Resolve schedule_id first so we can load rollover interval from DB
|
||||||
long nextScheduledTime = calculateNextScheduledTime(content.getScheduledTime());
|
Data inputDataForSchedule = getInputData();
|
||||||
|
boolean preserveStaticReminder = inputDataForSchedule.getBoolean("is_static_reminder", false);
|
||||||
|
String scheduleIdForRollover = inputDataForSchedule.getString("schedule_id");
|
||||||
|
if (scheduleIdForRollover == null || scheduleIdForRollover.isEmpty()) {
|
||||||
|
String notificationId = content.getId();
|
||||||
|
if (preserveStaticReminder && notificationId != null && !notificationId.isEmpty()) {
|
||||||
|
scheduleIdForRollover = notificationId;
|
||||||
|
} else if (notificationId != null && notificationId.startsWith("daily_")) {
|
||||||
|
scheduleIdForRollover = notificationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer rolloverMinutes = null;
|
||||||
|
if (scheduleIdForRollover != null && !scheduleIdForRollover.isEmpty()) {
|
||||||
|
com.timesafari.dailynotification.Schedule s = com.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), scheduleIdForRollover);
|
||||||
|
if (s != null && s.getRolloverIntervalMinutes() != null && s.getRolloverIntervalMinutes() > 0) {
|
||||||
|
rolloverMinutes = s.getRolloverIntervalMinutes();
|
||||||
|
Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + scheduleIdForRollover + " minutes=" + rolloverMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long nextScheduledTime;
|
||||||
|
if (rolloverMinutes != null && rolloverMinutes > 0) {
|
||||||
|
nextScheduledTime = addMinutesToTime(content.getScheduledTime(), rolloverMinutes);
|
||||||
|
Log.d(TAG, "DN|ROLLOVER_NEXT using_interval_minutes=" + rolloverMinutes + " next=" + nextScheduledTime);
|
||||||
|
} else {
|
||||||
|
nextScheduledTime = calculateNextScheduledTime(content.getScheduledTime());
|
||||||
|
}
|
||||||
|
|
||||||
// Check for existing notification at the same time to prevent duplicates
|
// Check for existing notification at the same time to prevent duplicates
|
||||||
DailyNotificationStorage legacyStorage = new DailyNotificationStorage(getApplicationContext());
|
DailyNotificationStorage legacyStorage = new DailyNotificationStorage(getApplicationContext());
|
||||||
@@ -561,36 +586,30 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve static reminder semantics across rollover; use stable schedule_id so reschedule cancels this alarm
|
|
||||||
Data inputData = getInputData();
|
|
||||||
boolean preserveStaticReminder = inputData.getBoolean("is_static_reminder", false);
|
|
||||||
String scheduleId = inputData.getString("schedule_id");
|
|
||||||
if (scheduleId == null || scheduleId.isEmpty()) {
|
|
||||||
String notificationId = content.getId();
|
|
||||||
if (preserveStaticReminder && notificationId != null && !notificationId.isEmpty()) {
|
|
||||||
scheduleId = notificationId;
|
|
||||||
} else if (notificationId != null && notificationId.startsWith("daily_")) {
|
|
||||||
scheduleId = notificationId;
|
|
||||||
} else {
|
|
||||||
scheduleId = "daily_rollover_" + System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String cronExpression = null;
|
String cronExpression = null;
|
||||||
String notificationId = content.getId();
|
String notificationId = content.getId();
|
||||||
|
String scheduleId = scheduleIdForRollover;
|
||||||
|
if (scheduleId == null || scheduleId.isEmpty()) {
|
||||||
|
scheduleId = "daily_rollover_" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate cron from current scheduled time (extract hour:minute)
|
// When using rollover interval, next time already set; otherwise compute from cron (tomorrow same time)
|
||||||
try {
|
if (rolloverMinutes == null || rolloverMinutes <= 0) {
|
||||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
try {
|
||||||
cal.setTimeInMillis(content.getScheduledTime());
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
int hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
|
cal.setTimeInMillis(content.getScheduledTime());
|
||||||
int minute = cal.get(java.util.Calendar.MINUTE);
|
int hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
|
||||||
cronExpression = String.format("%d %d * * *", minute, hour);
|
int minute = cal.get(java.util.Calendar.MINUTE);
|
||||||
|
cronExpression = String.format("%d %d * * *", minute, hour);
|
||||||
// Recalculate next run time from cron (tomorrow at same time)
|
nextScheduledTime = calculateNextRunTimeFromCron(cronExpression);
|
||||||
nextScheduledTime = calculateNextRunTimeFromCron(cronExpression);
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e);
|
||||||
Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e);
|
cronExpression = "0 9 * * *";
|
||||||
cronExpression = "0 9 * * *"; // Default to 9 AM
|
}
|
||||||
|
} else {
|
||||||
|
cronExpression = String.format("%d %d * * *",
|
||||||
|
java.util.Calendar.getInstance().get(java.util.Calendar.MINUTE),
|
||||||
|
java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config for next notification
|
// Create config for next notification
|
||||||
@@ -617,7 +636,10 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE,
|
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE,
|
||||||
false // skipPendingIntentIdempotence – rollover path does not skip
|
false // skipPendingIntentIdempotence – rollover path does not skip
|
||||||
);
|
);
|
||||||
|
if (scheduleId != null && !scheduleId.startsWith("daily_rollover_")) {
|
||||||
|
com.timesafari.dailynotification.ScheduleHelper.updateScheduleNextRunTimeBlocking(
|
||||||
|
getApplicationContext(), scheduleId, content.getScheduledTime(), nextScheduledTime);
|
||||||
|
}
|
||||||
// Log next scheduled time in readable format
|
// Log next scheduled time in readable format
|
||||||
String nextTimeStr = formatScheduledTime(nextScheduledTime);
|
String nextTimeStr = formatScheduledTime(nextScheduledTime);
|
||||||
Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr + " scheduleId=" + scheduleId);
|
Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr + " scheduleId=" + scheduleId);
|
||||||
@@ -734,7 +756,7 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext());
|
DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext());
|
||||||
NotificationContentEntity entity = new NotificationContentEntity(
|
NotificationContentEntity entity = new NotificationContentEntity(
|
||||||
content.getId() != null ? content.getId() : java.util.UUID.randomUUID().toString(),
|
content.getId() != null ? content.getId() : java.util.UUID.randomUUID().toString(),
|
||||||
"1.2.0",
|
"1.2.1",
|
||||||
null,
|
null,
|
||||||
"daily",
|
"daily",
|
||||||
content.getTitle(),
|
content.getTitle(),
|
||||||
@@ -771,6 +793,21 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add minutes to a timestamp (DST-safe via Calendar).
|
||||||
|
* Used for rollover interval (e.g. 10 minutes for testing).
|
||||||
|
*/
|
||||||
|
private long addMinutesToTime(long timeMillis, int minutes) {
|
||||||
|
try {
|
||||||
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
|
cal.setTimeInMillis(timeMillis);
|
||||||
|
cal.add(java.util.Calendar.MINUTE, minutes);
|
||||||
|
return cal.getTimeInMillis();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return timeMillis + (minutes * 60 * 1000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate next scheduled time with DST-safe handling
|
* Calculate next scheduled time with DST-safe handling
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ data class Schedule(
|
|||||||
val nextRunAt: Long? = null,
|
val nextRunAt: Long? = null,
|
||||||
val jitterMs: Int = 0,
|
val jitterMs: Int = 0,
|
||||||
val backoffPolicy: String = "exp",
|
val backoffPolicy: String = "exp",
|
||||||
val stateJson: String? = null
|
val stateJson: String? = null,
|
||||||
|
/** When > 0, next occurrence is this many minutes after current trigger (dev/testing). Null or 0 = 24h. */
|
||||||
|
val rolloverIntervalMinutes: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(tableName = "callbacks")
|
@Entity(tableName = "callbacks")
|
||||||
@@ -83,7 +85,7 @@ data class History(
|
|||||||
NotificationDeliveryEntity::class,
|
NotificationDeliveryEntity::class,
|
||||||
NotificationConfigEntity::class
|
NotificationConfigEntity::class
|
||||||
],
|
],
|
||||||
version = 2, // Incremented for unified schema
|
version = 3, // 3: add rollover_interval_minutes to schedules
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
@@ -118,7 +120,7 @@ abstract class DailyNotificationDatabase : RoomDatabase() {
|
|||||||
DailyNotificationDatabase::class.java,
|
DailyNotificationDatabase::class.java,
|
||||||
DATABASE_NAME
|
DATABASE_NAME
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2) // Migration from Kotlin-only to unified
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 1->2: unified; 2->3: rollover_interval_minutes
|
||||||
.addCallback(roomCallback)
|
.addCallback(roomCallback)
|
||||||
.build()
|
.build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
@@ -266,6 +268,15 @@ abstract class DailyNotificationDatabase : RoomDatabase() {
|
|||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from version 2 to 3: add rollover_interval_minutes to schedules
|
||||||
|
*/
|
||||||
|
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE schedules ADD COLUMN rollover_interval_minutes INTEGER")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.json.JSONObject
|
|||||||
* Implements exponential backoff and network constraints
|
* Implements exponential backoff and network constraints
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.2.0
|
* @version 1.3.0
|
||||||
*/
|
*/
|
||||||
class FetchWorker(
|
class FetchWorker(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
@@ -205,7 +205,7 @@ class FetchWorker(
|
|||||||
|
|
||||||
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
notificationId,
|
notificationId,
|
||||||
"1.2.0", // Plugin version
|
"1.3.0", // Plugin version
|
||||||
null, // timesafariDid - can be set if available
|
null, // timesafariDid - can be set if available
|
||||||
"daily",
|
"daily",
|
||||||
title,
|
title,
|
||||||
@@ -301,7 +301,7 @@ class FetchWorker(
|
|||||||
"timestamp": ${System.currentTimeMillis()},
|
"timestamp": ${System.currentTimeMillis()},
|
||||||
"content": "Daily notification content",
|
"content": "Daily notification content",
|
||||||
"source": "mock_generator",
|
"source": "mock_generator",
|
||||||
"version": "1.2.0"
|
"version": "1.3.0"
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
return mockData.toByteArray()
|
return mockData.toByteArray()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
* Implements TTL-at-fire logic and notification delivery
|
* Implements TTL-at-fire logic and notification delivery
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.2.0
|
* @version 1.3.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Source of schedule request - tracks which code path triggered scheduling
|
* Source of schedule request - tracks which code path triggered scheduling
|
||||||
@@ -251,7 +251,7 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
|
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
|
||||||
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
notificationId,
|
notificationId,
|
||||||
"1.2.0", // Plugin version
|
"1.3.0", // Plugin version
|
||||||
null, // timesafariDid - can be set if available
|
null, // timesafariDid - can be set if available
|
||||||
"daily",
|
"daily",
|
||||||
config.title,
|
config.title,
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ class ReactivationManager(private val context: Context) {
|
|||||||
markMissedNotificationForSchedule(schedule, nextRunTime, db)
|
markMissedNotificationForSchedule(schedule, nextRunTime, db)
|
||||||
missedCount++
|
missedCount++
|
||||||
|
|
||||||
// Schedule next occurrence if repeating
|
// Schedule next occurrence (use rollover interval if set, else 24h)
|
||||||
val nextOccurrence = calculateNextOccurrence(currentTime)
|
val nextOccurrence = calculateNextOccurrenceForSchedule(schedule, nextRunTime, currentTime)
|
||||||
rescheduleAlarmForBoot(context, schedule, nextOccurrence, db)
|
rescheduleAlarmForBoot(context, schedule, nextOccurrence, db)
|
||||||
rescheduledCount++
|
rescheduledCount++
|
||||||
|
|
||||||
@@ -238,10 +238,25 @@ class ReactivationManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextOccurrence(fromTime: Long): Long {
|
private fun calculateNextOccurrence(fromTime: Long): Long {
|
||||||
// For daily schedules, add 24 hours
|
|
||||||
// This is simplified - production should handle weekly/monthly patterns
|
|
||||||
return fromTime + (24 * 60 * 60 * 1000L)
|
return fromTime + (24 * 60 * 60 * 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next occurrence from a given trigger time. Uses schedule.rolloverIntervalMinutes when set and > 0 (dev/testing), else 24h.
|
||||||
|
* Advances until result > currentTime so we don't reschedule in the past.
|
||||||
|
*/
|
||||||
|
private fun calculateNextOccurrenceForSchedule(schedule: Schedule, fromTime: Long, currentTime: Long): Long {
|
||||||
|
val intervalMs = when {
|
||||||
|
schedule.rolloverIntervalMinutes != null && schedule.rolloverIntervalMinutes!! > 0 ->
|
||||||
|
schedule.rolloverIntervalMinutes!! * 60 * 1000L
|
||||||
|
else -> 24 * 60 * 60 * 1000L
|
||||||
|
}
|
||||||
|
var next = fromTime + intervalMs
|
||||||
|
while (next < currentTime) {
|
||||||
|
next += intervalMs
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun markMissedNotificationForSchedule(
|
private suspend fun markMissedNotificationForSchedule(
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
@@ -267,7 +282,7 @@ class ReactivationManager(private val context: Context) {
|
|||||||
// Create new notification content entry for missed alarm
|
// Create new notification content entry for missed alarm
|
||||||
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
notificationId,
|
notificationId,
|
||||||
"1.2.0", // Plugin version
|
"1.3.0", // Plugin version
|
||||||
null, // timesafariDid
|
null, // timesafariDid
|
||||||
"daily", // notificationType
|
"daily", // notificationType
|
||||||
"Daily Notification",
|
"Daily Notification",
|
||||||
@@ -1037,7 +1052,7 @@ class ReactivationManager(private val context: Context) {
|
|||||||
// Create new notification content entry for missed alarm
|
// Create new notification content entry for missed alarm
|
||||||
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
notificationId,
|
notificationId,
|
||||||
"1.2.0", // Plugin version
|
"1.3.0", // Plugin version
|
||||||
null, // timesafariDid
|
null, // timesafariDid
|
||||||
"daily", // notificationType
|
"daily", // notificationType
|
||||||
"Daily Notification",
|
"Daily Notification",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class DailyNotificationStorageRoom {
|
|||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
// Plugin version for migration tracking
|
// Plugin version for migration tracking
|
||||||
private static final String PLUGIN_VERSION = "1.2.0";
|
private static final String PLUGIN_VERSION = "1.2.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'DailyNotificationPlugin'
|
s.name = 'DailyNotificationPlugin'
|
||||||
s.version = '1.2.0'
|
s.version = '1.2.1'
|
||||||
s.summary = 'Daily Notification Plugin for Capacitor'
|
s.summary = 'Daily Notification Plugin for Capacitor'
|
||||||
s.license = 'MIT'
|
s.license = 'MIT'
|
||||||
s.homepage = 'https://github.com/timesafari/daily-notification-plugin'
|
s.homepage = 'https://github.com/timesafari/daily-notification-plugin'
|
||||||
|
|||||||
@@ -1118,12 +1118,14 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
let sound = call.getBool("sound", true)
|
let sound = call.getBool("sound", true)
|
||||||
let url = call.getString("url")
|
let url = call.getString("url")
|
||||||
let priority = call.getString("priority") ?? "default"
|
let priority = call.getString("priority") ?? "default"
|
||||||
|
let rawRollover = call.getInt("rolloverIntervalMinutes") ?? 0
|
||||||
|
let rolloverIntervalMinutes = rawRollover > 0 ? rawRollover : nil
|
||||||
|
|
||||||
// Calculate scheduled time (next occurrence at specified hour:minute)
|
// Calculate scheduled time (next occurrence at specified hour:minute)
|
||||||
let scheduledTime = calculateNextScheduledTime(hour: hour, minute: minute)
|
let scheduledTime = calculateNextScheduledTime(hour: hour, minute: minute)
|
||||||
let fetchedAt = Int64(Date().timeIntervalSince1970 * 1000) // Current time in milliseconds
|
let fetchedAt = Int64(Date().timeIntervalSince1970 * 1000) // Current time in milliseconds
|
||||||
|
|
||||||
// Create notification content
|
// Create notification content (persist rollover interval for dev/testing; survives app restart)
|
||||||
let content = NotificationContent(
|
let content = NotificationContent(
|
||||||
id: "daily_\(Date().timeIntervalSince1970)",
|
id: "daily_\(Date().timeIntervalSince1970)",
|
||||||
title: title,
|
title: title,
|
||||||
@@ -1132,7 +1134,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
fetchedAt: fetchedAt,
|
fetchedAt: fetchedAt,
|
||||||
url: url,
|
url: url,
|
||||||
payload: nil,
|
payload: nil,
|
||||||
etag: nil
|
etag: nil,
|
||||||
|
rolloverIntervalMinutes: rolloverIntervalMinutes
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delegate to ScheduleHelper for orchestration
|
// Delegate to ScheduleHelper for orchestration
|
||||||
|
|||||||
@@ -382,55 +382,24 @@ class DailyNotificationScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate next scheduled time from current scheduled time (24 hours later, DST-safe)
|
* Calculate next scheduled time from current (24h or rollover interval minutes). DST-safe.
|
||||||
*
|
* When rolloverIntervalMinutes > 0 (dev/testing), adds that many minutes; otherwise adds 24 hours.
|
||||||
* Matches Android calculateNextScheduledTime() functionality
|
|
||||||
* Handles DST transitions automatically using Calendar
|
|
||||||
*
|
|
||||||
* @param currentScheduledTime Current scheduled time in milliseconds
|
|
||||||
* @return Next scheduled time in milliseconds (24 hours later)
|
|
||||||
*
|
|
||||||
* TESTING: To test with shorter intervals (e.g., 2 minutes), change:
|
|
||||||
* - Line ~404: `.hour, value: 24` → `.minute, value: 2`
|
|
||||||
* - Line ~407: `(24 * 60 * 60 * 1000)` → `(2 * 60 * 1000)`
|
|
||||||
*/
|
*/
|
||||||
func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 {
|
func calculateNextScheduledTime(_ currentScheduledTime: Int64, rolloverIntervalMinutes: Int? = nil) -> Int64 {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
|
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
|
||||||
let currentTimeStr = formatTime(currentScheduledTime)
|
let currentTimeStr = formatTime(currentScheduledTime)
|
||||||
|
let addMinutes = (rolloverIntervalMinutes ?? 0) > 0 ? rolloverIntervalMinutes! : (24 * 60)
|
||||||
// Add 24 hours (handles DST transitions automatically)
|
guard let nextDate = calendar.date(byAdding: .minute, value: addMinutes, to: currentDate) else {
|
||||||
// TESTING: Change `.hour, value: 24` to `.minute, value: 2` for 2-minute testing
|
let fallbackTime = currentScheduledTime + (Int64(addMinutes) * 60 * 1000)
|
||||||
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
|
NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback add_minutes=\(addMinutes)")
|
||||||
// Fallback to simple 24-hour addition if calendar calculation fails
|
|
||||||
// TESTING: Change `(24 * 60 * 60 * 1000)` to `(2 * 60 * 1000)` for 2-minute testing
|
|
||||||
let fallbackTime = currentScheduledTime + (24 * 60 * 60 * 1000)
|
|
||||||
let fallbackTimeStr = formatTime(fallbackTime)
|
|
||||||
NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)")
|
|
||||||
print("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)")
|
|
||||||
return fallbackTime
|
return fallbackTime
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
|
let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||||
let nextTimeStr = formatTime(nextTime)
|
let nextTimeStr = formatTime(nextTime)
|
||||||
|
|
||||||
// Validate: Log DST transitions for debugging
|
|
||||||
let currentHour = calendar.component(.hour, from: currentDate)
|
|
||||||
let currentMinute = calendar.component(.minute, from: currentDate)
|
|
||||||
let nextHour = calendar.component(.hour, from: nextDate)
|
|
||||||
let nextMinute = calendar.component(.minute, from: nextDate)
|
|
||||||
|
|
||||||
if currentHour != nextHour || currentMinute != nextMinute {
|
|
||||||
NSLog("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))")
|
|
||||||
print("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the calculation result
|
|
||||||
let timeDiffMs = nextTime - currentScheduledTime
|
let timeDiffMs = nextTime - currentScheduledTime
|
||||||
let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
|
let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
|
||||||
NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
|
NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours)) rollover_min=\(rolloverIntervalMinutes ?? 0)")
|
||||||
print("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
|
|
||||||
|
|
||||||
return nextTime
|
return nextTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,16 +442,16 @@ class DailyNotificationScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate next occurrence using DST-safe calculation
|
// Calculate next occurrence (use stored rollover interval for dev/testing, else 24h)
|
||||||
var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime)
|
let rolloverMin = (content.rolloverIntervalMinutes ?? 0) > 0 ? content.rolloverIntervalMinutes : nil
|
||||||
|
var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime, rolloverIntervalMinutes: rolloverMin)
|
||||||
|
|
||||||
// If next scheduled time is in the past, keep calculating forward until we get a future time
|
// If next scheduled time is in the past, keep calculating forward until we get a future time
|
||||||
// This handles cases where the notification fired more than 2 minutes ago
|
// This handles cases where the notification fired more than 2 minutes ago
|
||||||
while nextScheduledTime < currentTime {
|
while nextScheduledTime < currentTime {
|
||||||
let nextTimeStr = formatTime(nextScheduledTime)
|
let nextTimeStr = formatTime(nextScheduledTime)
|
||||||
NSLog("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
|
NSLog("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
|
||||||
print("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
|
nextScheduledTime = calculateNextScheduledTime(nextScheduledTime, rolloverIntervalMinutes: rolloverMin)
|
||||||
nextScheduledTime = calculateNextScheduledTime(nextScheduledTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextScheduledTimeStr = formatTime(nextScheduledTime)
|
let nextScheduledTimeStr = formatTime(nextScheduledTime)
|
||||||
@@ -539,13 +508,14 @@ class DailyNotificationScheduler {
|
|||||||
let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
|
let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
|
||||||
let nextContent = NotificationContent(
|
let nextContent = NotificationContent(
|
||||||
id: nextId,
|
id: nextId,
|
||||||
title: content.title, // Will be updated by prefetch
|
title: content.title,
|
||||||
body: content.body, // Will be updated by prefetch
|
body: content.body,
|
||||||
scheduledTime: nextScheduledTime,
|
scheduledTime: nextScheduledTime,
|
||||||
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
||||||
url: content.url,
|
url: content.url,
|
||||||
payload: content.payload,
|
payload: content.payload,
|
||||||
etag: content.etag
|
etag: content.etag,
|
||||||
|
rolloverIntervalMinutes: content.rolloverIntervalMinutes
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schedule the next notification
|
// Schedule the next notification
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class NotificationContent: Codable {
|
|||||||
let url: String?
|
let url: String?
|
||||||
let payload: [String: Any]?
|
let payload: [String: Any]?
|
||||||
let etag: String?
|
let etag: String?
|
||||||
|
/** When > 0, next occurrence is this many minutes after trigger (dev/testing). Nil/0 = 24h. Persisted for rollover and recovery. */
|
||||||
|
var rolloverIntervalMinutes: Int?
|
||||||
|
|
||||||
// Phase 2: Delivery tracking properties
|
// Phase 2: Delivery tracking properties
|
||||||
var deliveryStatus: String? // e.g., "scheduled", "delivered", "missed", "error"
|
var deliveryStatus: String? // e.g., "scheduled", "delivered", "missed", "error"
|
||||||
@@ -43,6 +45,7 @@ class NotificationContent: Codable {
|
|||||||
case url
|
case url
|
||||||
case payload
|
case payload
|
||||||
case etag
|
case etag
|
||||||
|
case rolloverIntervalMinutes
|
||||||
case deliveryStatus
|
case deliveryStatus
|
||||||
case lastDeliveryAttempt
|
case lastDeliveryAttempt
|
||||||
}
|
}
|
||||||
@@ -64,6 +67,7 @@ class NotificationContent: Codable {
|
|||||||
payload = nil
|
payload = nil
|
||||||
}
|
}
|
||||||
etag = try container.decodeIfPresent(String.self, forKey: .etag)
|
etag = try container.decodeIfPresent(String.self, forKey: .etag)
|
||||||
|
rolloverIntervalMinutes = try container.decodeIfPresent(Int.self, forKey: .rolloverIntervalMinutes)
|
||||||
deliveryStatus = try container.decodeIfPresent(String.self, forKey: .deliveryStatus)
|
deliveryStatus = try container.decodeIfPresent(String.self, forKey: .deliveryStatus)
|
||||||
lastDeliveryAttempt = try container.decodeIfPresent(Int64.self, forKey: .lastDeliveryAttempt)
|
lastDeliveryAttempt = try container.decodeIfPresent(Int64.self, forKey: .lastDeliveryAttempt)
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,7 @@ class NotificationContent: Codable {
|
|||||||
try container.encode(payloadString, forKey: .payload)
|
try container.encode(payloadString, forKey: .payload)
|
||||||
}
|
}
|
||||||
try container.encodeIfPresent(etag, forKey: .etag)
|
try container.encodeIfPresent(etag, forKey: .etag)
|
||||||
|
try container.encodeIfPresent(rolloverIntervalMinutes, forKey: .rolloverIntervalMinutes)
|
||||||
try container.encodeIfPresent(deliveryStatus, forKey: .deliveryStatus)
|
try container.encodeIfPresent(deliveryStatus, forKey: .deliveryStatus)
|
||||||
try container.encodeIfPresent(lastDeliveryAttempt, forKey: .lastDeliveryAttempt)
|
try container.encodeIfPresent(lastDeliveryAttempt, forKey: .lastDeliveryAttempt)
|
||||||
}
|
}
|
||||||
@@ -100,20 +105,21 @@ class NotificationContent: Codable {
|
|||||||
* @param url URL for content fetching
|
* @param url URL for content fetching
|
||||||
* @param payload Additional payload data
|
* @param payload Additional payload data
|
||||||
* @param etag ETag for HTTP caching
|
* @param etag ETag for HTTP caching
|
||||||
|
* @param rolloverIntervalMinutes When > 0, next occurrence is this many minutes after trigger (dev/testing). Nil/0 = 24h.
|
||||||
* @param deliveryStatus Delivery status (optional, Phase 2)
|
* @param deliveryStatus Delivery status (optional, Phase 2)
|
||||||
* @param lastDeliveryAttempt Last delivery attempt timestamp (optional, Phase 2)
|
* @param lastDeliveryAttempt Last delivery attempt timestamp (optional, Phase 2)
|
||||||
*/
|
*/
|
||||||
init(id: String,
|
init(id: String,
|
||||||
title: String?,
|
title: String?,
|
||||||
body: String?,
|
body: String?,
|
||||||
scheduledTime: Int64,
|
scheduledTime: Int64,
|
||||||
fetchedAt: Int64,
|
fetchedAt: Int64,
|
||||||
url: String?,
|
url: String?,
|
||||||
payload: [String: Any]?,
|
payload: [String: Any]?,
|
||||||
etag: String?,
|
etag: String?,
|
||||||
|
rolloverIntervalMinutes: Int? = nil,
|
||||||
deliveryStatus: String? = nil,
|
deliveryStatus: String? = nil,
|
||||||
lastDeliveryAttempt: Int64? = nil) {
|
lastDeliveryAttempt: Int64? = nil) {
|
||||||
|
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.body = body
|
self.body = body
|
||||||
@@ -122,6 +128,7 @@ class NotificationContent: Codable {
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.etag = etag
|
self.etag = etag
|
||||||
|
self.rolloverIntervalMinutes = rolloverIntervalMinutes
|
||||||
self.deliveryStatus = deliveryStatus
|
self.deliveryStatus = deliveryStatus
|
||||||
self.lastDeliveryAttempt = lastDeliveryAttempt
|
self.lastDeliveryAttempt = lastDeliveryAttempt
|
||||||
}
|
}
|
||||||
@@ -259,6 +266,7 @@ class NotificationContent: Codable {
|
|||||||
lastDeliveryAttempt = nil
|
lastDeliveryAttempt = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rollover = (dict["rolloverIntervalMinutes"] as? NSNumber)?.intValue
|
||||||
return NotificationContent(
|
return NotificationContent(
|
||||||
id: id,
|
id: id,
|
||||||
title: dict["title"] as? String,
|
title: dict["title"] as? String,
|
||||||
@@ -268,6 +276,7 @@ class NotificationContent: Codable {
|
|||||||
url: dict["url"] as? String,
|
url: dict["url"] as? String,
|
||||||
payload: dict["payload"] as? [String: Any],
|
payload: dict["payload"] as? [String: Any],
|
||||||
etag: dict["etag"] as? String,
|
etag: dict["etag"] as? String,
|
||||||
|
rolloverIntervalMinutes: rollover,
|
||||||
deliveryStatus: dict["deliveryStatus"] as? String,
|
deliveryStatus: dict["deliveryStatus"] as? String,
|
||||||
lastDeliveryAttempt: lastDeliveryAttempt
|
lastDeliveryAttempt: lastDeliveryAttempt
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@timesafari/daily-notification-plugin",
|
"name": "@timesafari/daily-notification-plugin",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms",
|
"description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms",
|
||||||
"main": "dist/plugin.js",
|
"main": "dist/plugin.js",
|
||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.js",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Aligned with Android implementation and test requirements
|
* Aligned with Android implementation and test requirements
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.2.0 (see package.json for source of truth)
|
* @version 1.3.0 (see package.json for source of truth)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import SPI types from content-fetcher.ts
|
// Import SPI types from content-fetcher.ts
|
||||||
@@ -82,6 +82,12 @@ export interface NotificationOptions {
|
|||||||
retryInterval?: number;
|
retryInterval?: number;
|
||||||
offlineFallback?: boolean;
|
offlineFallback?: boolean;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Optional rollover interval in minutes for dev/testing.
|
||||||
|
* When set (e.g. 10), the next occurrence is scheduled this many minutes after the current trigger
|
||||||
|
* instead of 24 hours. Persisted with the schedule so it survives reboot. When absent or <= 0, use 24h.
|
||||||
|
*/
|
||||||
|
rolloverIntervalMinutes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Provides structured logging, event codes, and health monitoring
|
* Provides structured logging, event codes, and health monitoring
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.2.0
|
* @version 1.3.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* This implementation provides clear error messages for all methods.
|
* This implementation provides clear error messages for all methods.
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.2.0
|
* @version 1.3.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
Reference in New Issue
Block a user