feat(android): schedule batch notifications with predictive_<epochMillis> IDs
Add scheduleNotifications so Android predictive scheduling matches clearPredictiveNotifications and iOS batch API behavior.
This commit is contained in:
@@ -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)
|
||||
*
|
||||
|
||||
+15
@@ -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
|
||||
|
||||
@@ -602,6 +602,11 @@ export interface DailyNotificationPlugin {
|
||||
|
||||
getLastNotification(): Promise<NotificationResponse | null>;
|
||||
cancelAllNotifications(): Promise<void>;
|
||||
/**
|
||||
* Add one-shot API/predictive notifications at epoch-ms timestamps (additive).
|
||||
* Android uses `predictive_<timestamp>` schedule IDs; caller should clear first when replacing a batch.
|
||||
*/
|
||||
scheduleNotifications(options: { timestamps: number[] }): Promise<void>;
|
||||
/**
|
||||
* Android only: cancel API/predictive notification schedules (`predictive_*`) without
|
||||
* affecting Daily Reminders, user schedules, dual schedules, or fetch jobs.
|
||||
|
||||
@@ -118,6 +118,10 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async scheduleNotifications(): Promise<void> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async clearPredictiveNotifications(): Promise<void> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user