From eca2bbf947848a0e483f3acb5c1990bc48c2b9cd Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 8 Jun 2026 19:28:12 +0800 Subject: [PATCH] feat(android): schedule batch notifications with predictive_ IDs Add scheduleNotifications so Android predictive scheduling matches clearPredictiveNotifications and iOS batch API behavior. --- .../DailyNotificationPlugin.kt | 97 +++++++++++++++++++ .../ClearPredictiveSchedulesTest.kt | 15 +++ src/definitions.ts | 5 + src/web.ts | 4 + 4 files changed, 121 insertions(+) diff --git a/android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.kt index e154ca0..56f2d42 100644 --- a/android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -19,6 +19,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Data import java.util.concurrent.TimeUnit import org.timesafari.dailynotification.DailyNotificationFetchWorker +import com.getcapacitor.JSArray import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.getcapacitor.PluginCall @@ -743,6 +744,46 @@ open class DailyNotificationPlugin : Plugin() { } } + /** + * Add one-shot API/predictive notifications at epoch-ms timestamps (additive; does not clear others). + * Caller should invoke [clearPredictiveNotifications] first when replacing a full batch. + */ + @PluginMethod + fun scheduleNotifications(call: PluginCall) { + val timestampsArray = call.getArray("timestamps") + if (timestampsArray == null) { + call.reject("Missing timestamps") + return + } + + CoroutineScope(Dispatchers.IO).launch { + try { + if (context == null) { + return@launch call.reject("Context not available") + } + Log.i(TAG, "DNP-BATCH: scheduleNotifications — additive scheduling for ${timestampsArray.length()} timestamp(s)") + + var scheduledCount = 0 + for (i in 0 until timestampsArray.length()) { + val raw = timestampsArray.opt(i) + val epochMillis = when (raw) { + is Number -> raw.toLong() + else -> continue + } + if (ScheduleHelper.schedulePredictiveNotification(context!!, getDatabase(), epochMillis)) { + scheduledCount++ + } + } + + Log.i(TAG, "DNP-BATCH: scheduleNotifications done scheduled=$scheduledCount/${timestampsArray.length()}") + call.resolve() + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule notifications", e) + call.reject("Failed to schedule notifications: ${e.message}") + } + } + } + /** * Cancel only API/predictive notification schedules (`predictive_*`). * Does not affect Daily Reminders, user-created schedules, dual schedules, or fetch jobs. @@ -2957,6 +2998,62 @@ object ScheduleHelper { } } + /** + * Schedule one API/predictive notification at an epoch-ms trigger time. + * Uses the predictive_ ID namespace so [clearPredictiveSchedules] can replace batches safely. + * + * @return true when scheduled, false when timestamp is stale (in the past) + */ + suspend fun schedulePredictiveNotification( + context: Context, + database: DailyNotificationDatabase, + epochMillis: Long, + title: String = "Reminder", + body: String = "You have a scheduled notification" + ): Boolean { + if (epochMillis <= System.currentTimeMillis()) { + Log.d("ScheduleHelper", "DNP-BATCH: skip stale timestamp ts=$epochMillis (not in the future)") + return false + } + + // Predictive/API notifications use the predictive_ namespace so they can be safely + // replaced without affecting Daily Reminder schedules. + val scheduleId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}${epochMillis}" + + val config = UserNotificationConfig( + enabled = true, + schedule = scheduleId, + title = title, + body = body, + sound = true, + vibration = true, + priority = "default" + ) + + // Persist before arming so NotifyReceiver updates this row, not an unrelated notify schedule. + createSchedule( + database, + Schedule( + id = scheduleId, + kind = "notify", + enabled = true, + nextRunAt = epochMillis + ) + ) + + NotifyReceiver.scheduleExactNotification( + context, + epochMillis, + config, + reminderId = scheduleId, + scheduleId = scheduleId, + source = ScheduleSource.INITIAL_SETUP + ) + + Log.i("ScheduleHelper", "DNP-BATCH: scheduled ts=$epochMillis id=$scheduleId") + return true + } + /** * Schedule user notification (alarm + database) * diff --git a/android/src/test/java/org/timesafari/dailynotification/ClearPredictiveSchedulesTest.kt b/android/src/test/java/org/timesafari/dailynotification/ClearPredictiveSchedulesTest.kt index def92cc..f7b0607 100644 --- a/android/src/test/java/org/timesafari/dailynotification/ClearPredictiveSchedulesTest.kt +++ b/android/src/test/java/org/timesafari/dailynotification/ClearPredictiveSchedulesTest.kt @@ -31,6 +31,21 @@ class ClearPredictiveSchedulesTest { database.close() } + @Test + fun schedulePredictiveNotification_usesPredictiveIdNamespace() { + val epochMillis = System.currentTimeMillis() + 300_000 + val expectedId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}${epochMillis}" + + val scheduled = runBlocking { + ScheduleHelper.schedulePredictiveNotification(context, database, epochMillis) + } + + assertEquals(true, scheduled) + runBlocking { + assertNotNull(database.scheduleDao().getById(expectedId)) + } + } + @Test fun clearPredictiveSchedules_removesOnlyPredictiveAndPreservesDailyReminder() { val dailyReminderId = DailyNotificationConstants.DEFAULT_SCHEDULE_ID diff --git a/src/definitions.ts b/src/definitions.ts index f4af2a9..7d38983 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -602,6 +602,11 @@ export interface DailyNotificationPlugin { getLastNotification(): Promise; cancelAllNotifications(): Promise; + /** + * Add one-shot API/predictive notifications at epoch-ms timestamps (additive). + * Android uses `predictive_` schedule IDs; caller should clear first when replacing a batch. + */ + scheduleNotifications(options: { timestamps: number[] }): Promise; /** * Android only: cancel API/predictive notification schedules (`predictive_*`) without * affecting Daily Reminders, user schedules, dual schedules, or fetch jobs. diff --git a/src/web.ts b/src/web.ts index 0b65387..5d5f5cc 100644 --- a/src/web.ts +++ b/src/web.ts @@ -118,6 +118,10 @@ export class DailyNotificationWeb implements DailyNotificationPlugin { this.throwNotSupported(); } + async scheduleNotifications(): Promise { + this.throwNotSupported(); + } + async clearPredictiveNotifications(): Promise { this.throwNotSupported(); }