remove scheduleApiNotifications so the plugin cannot create new api_* schedules

This commit is contained in:
Jose Olarte III
2026-09-23 20:09:12 +08:00
parent 69b243c09b
commit cf110f69fb
7 changed files with 20 additions and 184 deletions
+6
View File
@@ -5,6 +5,12 @@ All notable changes to the Daily Notification Plugin will be documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Removed
- **Breaking**: Retired `scheduleApiNotifications` / `scheduleApiNotification`. The plugin no longer creates or schedules new `api_*` notifications. `clearApiNotifications` remains for the host `legacyApiNotificationsCleanup` migration.
## [4.0.1] - 2026-06-09 ## [4.0.1] - 2026-06-09
### Fixed ### Fixed
@@ -170,8 +170,8 @@ object DailyNotificationConstants {
const val DUAL_NOTIFY_SCHEDULE_ID_PREFIX = "dual_notify_" const val DUAL_NOTIFY_SCHEDULE_ID_PREFIX = "dual_notify_"
/** /**
* Prefix for API-managed batch notification schedule IDs (`api_<epochMillis>`). * Prefix for leftover API-managed batch notification schedule IDs (`api_<epochMillis>`).
* Matches iOS [apiNotificationPrefix]; must not overlap daily reminders or dual schedules. * Used only by [ScheduleHelper.clearApiSchedules]; new schedules are no longer created in this namespace.
*/ */
const val API_SCHEDULE_ID_PREFIX = "api_" const val API_SCHEDULE_ID_PREFIX = "api_"
@@ -19,7 +19,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.Data import androidx.work.Data
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.timesafari.dailynotification.DailyNotificationFetchWorker import org.timesafari.dailynotification.DailyNotificationFetchWorker
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.getcapacitor.Plugin import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall import com.getcapacitor.PluginCall
@@ -750,48 +749,9 @@ open class DailyNotificationPlugin : Plugin() {
} }
/** /**
* Add one-shot API-managed notifications at epoch-ms timestamps (additive; does not clear others). * Cancel leftover API-managed notification schedules (`api_*`).
* Caller should invoke [clearApiNotifications] first when replacing a full batch.
*/
@PluginMethod
fun scheduleApiNotifications(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: scheduleApiNotifications — 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.scheduleApiNotification(context!!, getDatabase(), epochMillis)) {
scheduledCount++
}
}
Log.i(TAG, "DNP-BATCH: scheduleApiNotifications 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-managed notification schedules (`api_*`).
* Does not affect Daily Reminders, user-created schedules, dual schedules, or fetch jobs. * Does not affect Daily Reminders, user-created schedules, dual schedules, or fetch jobs.
* Kept for the host `legacyApiNotificationsCleanup` migration; this plugin no longer creates `api_*` schedules.
*/ */
@PluginMethod @PluginMethod
fun clearApiNotifications(call: PluginCall) { fun clearApiNotifications(call: PluginCall) {
@@ -3011,62 +2971,6 @@ object ScheduleHelper {
} }
} }
/**
* Schedule one API-managed notification at an epoch-ms trigger time.
* Uses the api_ ID namespace so [clearApiSchedules] can replace batches safely.
*
* @return true when scheduled, false when timestamp is stale (in the past)
*/
suspend fun scheduleApiNotification(
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
}
// API-managed notifications use the api_ namespace so they can be safely
// replaced without affecting Daily Reminder schedules.
val scheduleId = "${DailyNotificationConstants.API_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) * Schedule user notification (alarm + database)
* *
@@ -31,21 +31,6 @@ class ClearApiSchedulesTest {
database.close() database.close()
} }
@Test
fun scheduleApiNotification_usesApiIdNamespace() {
val epochMillis = System.currentTimeMillis() + 300_000
val expectedId = "${DailyNotificationConstants.API_SCHEDULE_ID_PREFIX}${epochMillis}"
val scheduled = runBlocking {
ScheduleHelper.scheduleApiNotification(context, database, epochMillis)
}
assertEquals(true, scheduled)
runBlocking {
assertNotNull(database.scheduleDao().getById(expectedId))
}
}
@Test @Test
fun clearApiSchedules_removesOnlyApiManagedAndPreservesDailyReminder() { fun clearApiSchedules_removesOnlyApiManagedAndPreservesDailyReminder() {
val dailyReminderId = DailyNotificationConstants.DEFAULT_SCHEDULE_ID val dailyReminderId = DailyNotificationConstants.DEFAULT_SCHEDULE_ID
+6 -59
View File
@@ -32,7 +32,8 @@ public class DailyNotificationPlugin: CAPPlugin {
// Background task identifiers // Background task identifiers
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch" private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify" private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
/// Prefix for API-managed batch notification identifiers: `api_\(epochMillis)`. /// Prefix for leftover API-managed batch notification identifiers: `api_\(epochMillis)`.
/// Used by `clearApiNotifications` and Daily Reminder legacy-id cancellation; new `api_*` requests are not scheduled.
private let apiNotificationPrefix = "api_" private let apiNotificationPrefix = "api_"
/// Prefix for Daily Reminder UNNotificationRequest identifiers: `daily_\(logicalReminderId)`. /// Prefix for Daily Reminder UNNotificationRequest identifiers: `daily_\(logicalReminderId)`.
private let dailyReminderNotificationPrefix = "daily_" private let dailyReminderNotificationPrefix = "daily_"
@@ -1123,9 +1124,9 @@ public class DailyNotificationPlugin: CAPPlugin {
notificationCenter.removePendingNotificationRequests(withIdentifiers: ids) notificationCenter.removePendingNotificationRequests(withIdentifiers: ids)
} }
// MARK: - API batch notifications (replace-all UNUserNotificationCenter) // MARK: - Leftover API batch notifications (clear-only; scheduling retired)
/// Removes only pending and delivered API-managed notifications (`api_*`). Does not touch Daily Reminders (`daily_*`), dual/org IDs, or other stacks. /// Removes only pending and delivered leftover API-managed notifications (`api_*`). Does not touch Daily Reminders (`daily_*`), dual/org IDs, or other stacks.
@objc func clearApiNotifications(_ call: CAPPluginCall) { @objc func clearApiNotifications(_ call: CAPPluginCall) {
NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only") NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
print("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only") print("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
@@ -1156,59 +1157,6 @@ public class DailyNotificationPlugin: CAPPlugin {
} }
} }
/// Add one-shot reminders at epoch-ms timestamps. Does not remove other requests; identical IDs replace pending entries. Caller should clear first if needed.
/// Adds/overwrites API-managed notifications using deterministic IDs.
/// Does NOT clear existing notifications. Caller is responsible for lifecycle.
@objc func scheduleApiNotifications(_ call: CAPPluginCall) {
guard let timestamps = call.getArray("timestamps", Double.self) else {
call.reject("Missing timestamps")
return
}
NSLog("DNP-BATCH: scheduleApiNotifications — additive scheduling for \(timestamps.count) timestamp(s)")
print("DNP-BATCH: scheduleApiNotifications — additive scheduling for \(timestamps.count) timestamp(s)")
for ts in timestamps {
let date = Date(timeIntervalSince1970: ts / 1000)
guard date.timeIntervalSinceNow > 0 else {
NSLog("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
print("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
continue
}
let interval = date.timeIntervalSinceNow
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: interval,
repeats: false
)
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "You have a scheduled notification"
let id = "\(apiNotificationPrefix)\(Int(ts))"
NSLog("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")
print("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")
let request = UNNotificationRequest(
identifier: id,
content: content,
trigger: trigger
)
notificationCenter.add(request) { error in
if let error = error {
NSLog("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
print("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
}
}
}
call.resolve()
}
// MARK: - Static Daily Reminder Methods // MARK: - Static Daily Reminder Methods
@objc func scheduleDailyReminder(_ call: CAPPluginCall) { @objc func scheduleDailyReminder(_ call: CAPPluginCall) {
@@ -1712,7 +1660,7 @@ public class DailyNotificationPlugin: CAPPlugin {
let fireDate = Date().addingTimeInterval(TimeInterval(validSeconds)) let fireDate = Date().addingTimeInterval(TimeInterval(validSeconds))
let epochMillis = Int64(fireDate.timeIntervalSince1970 * 1000) let epochMillis = Int64(fireDate.timeIntervalSince1970 * 1000)
let requestId = apiNotificationId(epochMillis: epochMillis) let requestId = "test_\(epochMillis)"
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: requestId, identifier: requestId,
content: notificationContent, content: notificationContent,
@@ -2803,9 +2751,8 @@ extension DailyNotificationPlugin {
methods.append(CAPPluginMethod(name: "getScheduledReminders", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "getScheduledReminders", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "updateDailyReminder", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "updateDailyReminder", returnType: CAPPluginReturnPromise))
// API batch notifications (replace-all UNUserNotificationCenter) // Leftover API batch notifications (clear-only; scheduling retired)
methods.append(CAPPluginMethod(name: "clearApiNotifications", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "clearApiNotifications", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "scheduleApiNotifications", returnType: CAPPluginReturnPromise))
// Dual scheduling methods // Dual scheduling methods
methods.append(CAPPluginMethod(name: "scheduleContentFetch", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "scheduleContentFetch", returnType: CAPPluginReturnPromise))
+4 -6
View File
@@ -603,14 +603,12 @@ export interface DailyNotificationPlugin {
getLastNotification(): Promise<NotificationResponse | null>; getLastNotification(): Promise<NotificationResponse | null>;
cancelAllNotifications(): Promise<void>; cancelAllNotifications(): Promise<void>;
/** /**
* Add one-shot API-managed notifications at epoch-ms timestamps (additive). * Cancel leftover API-managed notifications (`api_*`) without affecting Daily Reminders
* Uses `api_<timestamp>` identifiers; caller should clear first when replacing a batch.
*/
scheduleApiNotifications(options: { timestamps: number[] }): Promise<void>;
/**
* Cancel API-managed notifications (`api_*`) without affecting Daily Reminders
* (iOS: `daily_*`; Android: `daily_notification` / caller id), user schedules, * (iOS: `daily_*`; Android: `daily_notification` / caller id), user schedules,
* dual schedules, or fetch jobs. * dual schedules, or fetch jobs.
*
* Kept for the one-time `legacyApiNotificationsCleanup` host migration.
* New `api_*` notifications are no longer created or scheduled by this plugin.
*/ */
clearApiNotifications(): Promise<void>; clearApiNotifications(): Promise<void>;
getNotificationStatus(): Promise<NotificationStatus>; getNotificationStatus(): Promise<NotificationStatus>;
-4
View File
@@ -118,10 +118,6 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
this.throwNotSupported(); this.throwNotSupported();
} }
async scheduleApiNotifications(): Promise<void> {
this.throwNotSupported();
}
async clearApiNotifications(): Promise<void> { async clearApiNotifications(): Promise<void> {
this.throwNotSupported(); this.throwNotSupported();
} }