refactor: rename internal notification ID prefix from predictive_ to api_

This commit is contained in:
Jose Olarte III
2026-06-09 17:27:15 +08:00
parent eca2bbf947
commit ceaa4ada66
5 changed files with 69 additions and 69 deletions

View File

@@ -170,10 +170,10 @@ object DailyNotificationConstants {
const val DUAL_NOTIFY_SCHEDULE_ID_PREFIX = "dual_notify_"
/**
* Prefix for API/predictive batch notification schedule IDs (`predictive_<epochMillis>`).
* Matches iOS [predictiveNotificationPrefix]; must not overlap daily reminders or dual schedules.
* Prefix for API-managed batch notification schedule IDs (`api_<epochMillis>`).
* Matches iOS [apiNotificationPrefix]; must not overlap daily reminders or dual schedules.
*/
const val PREDICTIVE_SCHEDULE_ID_PREFIX = "predictive_"
const val API_SCHEDULE_ID_PREFIX = "api_"
// ============================================================
// Request Code Versioning

View File

@@ -745,7 +745,7 @@ open class DailyNotificationPlugin : Plugin() {
}
/**
* Add one-shot API/predictive notifications at epoch-ms timestamps (additive; does not clear others).
* Add one-shot API-managed notifications at epoch-ms timestamps (additive; does not clear others).
* Caller should invoke [clearPredictiveNotifications] first when replacing a full batch.
*/
@PluginMethod
@@ -770,7 +770,7 @@ open class DailyNotificationPlugin : Plugin() {
is Number -> raw.toLong()
else -> continue
}
if (ScheduleHelper.schedulePredictiveNotification(context!!, getDatabase(), epochMillis)) {
if (ScheduleHelper.scheduleApiNotification(context!!, getDatabase(), epochMillis)) {
scheduledCount++
}
}
@@ -785,7 +785,7 @@ open class DailyNotificationPlugin : Plugin() {
}
/**
* Cancel only API/predictive notification schedules (`predictive_*`).
* Cancel only API-managed notification schedules (`api_*`).
* Does not affect Daily Reminders, user-created schedules, dual schedules, or fetch jobs.
*/
@PluginMethod
@@ -795,13 +795,13 @@ open class DailyNotificationPlugin : Plugin() {
if (context == null) {
return@launch call.reject("Context not available")
}
Log.i("DailyNotificationPlugin", "Clearing predictive notifications")
val cleared = ScheduleHelper.clearPredictiveSchedules(context!!, getDatabase())
Log.i("DailyNotificationPlugin", "Cleared $cleared predictive schedules")
Log.i("DailyNotificationPlugin", "Clearing API-managed notifications")
val cleared = ScheduleHelper.clearApiSchedules(context!!, getDatabase())
Log.i("DailyNotificationPlugin", "Cleared $cleared API-managed schedule(s)")
call.resolve()
} catch (e: Exception) {
Log.e(TAG, "Failed to clear predictive notifications", e)
call.reject("Failed to clear predictive notifications: ${e.message}")
Log.e(TAG, "Failed to clear API-managed notifications", e)
call.reject("Failed to clear API-managed notifications: ${e.message}")
}
}
}
@@ -2999,12 +2999,12 @@ object ScheduleHelper {
}
/**
* Schedule one API/predictive notification at an epoch-ms trigger time.
* Uses the predictive_ ID namespace so [clearPredictiveSchedules] can replace batches safely.
* 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 schedulePredictiveNotification(
suspend fun scheduleApiNotification(
context: Context,
database: DailyNotificationDatabase,
epochMillis: Long,
@@ -3016,9 +3016,9 @@ object ScheduleHelper {
return false
}
// Predictive/API notifications use the predictive_ namespace so they can be safely
// API-managed notifications use the api_ namespace so they can be safely
// replaced without affecting Daily Reminder schedules.
val scheduleId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}${epochMillis}"
val scheduleId = "${DailyNotificationConstants.API_SCHEDULE_ID_PREFIX}${epochMillis}"
val config = UserNotificationConfig(
enabled = true,
@@ -3131,24 +3131,24 @@ object ScheduleHelper {
}
/**
* Cancel only predictive/API notification schedules (`predictive_*` prefix).
* Cancel only API-managed notification schedules (`api_*` prefix).
* Removes matching alarms, persisted schedules, and notification content.
* Does not disable Daily Reminders, user schedules, dual schedules, or fetch jobs.
*/
suspend fun clearPredictiveSchedules(context: Context, database: DailyNotificationDatabase): Int {
suspend fun clearApiSchedules(context: Context, database: DailyNotificationDatabase): Int {
return try {
val prefix = DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX
val prefix = DailyNotificationConstants.API_SCHEDULE_ID_PREFIX
val all = database.scheduleDao().getAll()
val predictiveSchedules = all.filter { it.id.startsWith(prefix) }
if (predictiveSchedules.isEmpty()) {
Log.d("ScheduleHelper", "clearPredictiveSchedules: no predictive schedules found")
val apiSchedules = all.filter { it.id.startsWith(prefix) }
if (apiSchedules.isEmpty()) {
Log.d("ScheduleHelper", "clearApiSchedules: no API-managed schedules found")
return 0
}
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
predictiveSchedules.forEach { schedule ->
apiSchedules.forEach { schedule ->
NotifyReceiver.cancelNotification(
context,
scheduleId = schedule.id,
@@ -3157,7 +3157,7 @@ object ScheduleHelper {
notificationManager.cancel(schedule.id.hashCode())
}
predictiveSchedules.forEach { database.scheduleDao().deleteById(it.id) }
apiSchedules.forEach { database.scheduleDao().deleteById(it.id) }
database.notificationContentDao().getAllNotifications()
.filter { it.id.startsWith(prefix) }
@@ -3166,10 +3166,10 @@ object ScheduleHelper {
notificationManager.cancel(content.id.hashCode())
}
Log.i("ScheduleHelper", "clearPredictiveSchedules: cancelled and removed ${predictiveSchedules.size} predictive schedule(s)")
predictiveSchedules.size
Log.i("ScheduleHelper", "clearApiSchedules: cancelled and removed ${apiSchedules.size} API-managed schedule(s)")
apiSchedules.size
} catch (e: Exception) {
Log.e("ScheduleHelper", "clearPredictiveSchedules failed", e)
Log.e("ScheduleHelper", "clearApiSchedules failed", e)
0
}
}

View File

@@ -15,7 +15,7 @@ import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28])
class ClearPredictiveSchedulesTest {
class ClearApiSchedulesTest {
private lateinit var context: Context
private lateinit var database: DailyNotificationDatabase
@@ -32,12 +32,12 @@ class ClearPredictiveSchedulesTest {
}
@Test
fun schedulePredictiveNotification_usesPredictiveIdNamespace() {
fun scheduleApiNotification_usesApiIdNamespace() {
val epochMillis = System.currentTimeMillis() + 300_000
val expectedId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}${epochMillis}"
val expectedId = "${DailyNotificationConstants.API_SCHEDULE_ID_PREFIX}${epochMillis}"
val scheduled = runBlocking {
ScheduleHelper.schedulePredictiveNotification(context, database, epochMillis)
ScheduleHelper.scheduleApiNotification(context, database, epochMillis)
}
assertEquals(true, scheduled)
@@ -47,9 +47,9 @@ class ClearPredictiveSchedulesTest {
}
@Test
fun clearPredictiveSchedules_removesOnlyPredictiveAndPreservesDailyReminder() {
fun clearApiSchedules_removesOnlyApiManagedAndPreservesDailyReminder() {
val dailyReminderId = DailyNotificationConstants.DEFAULT_SCHEDULE_ID
val predictiveId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}1740000000000"
val apiId = "${DailyNotificationConstants.API_SCHEDULE_ID_PREFIX}1740000000000"
val userScheduleId = "notify_1740000000001"
runBlocking {
@@ -64,7 +64,7 @@ class ClearPredictiveSchedulesTest {
)
database.scheduleDao().upsert(
Schedule(
id = predictiveId,
id = apiId,
kind = "notify",
enabled = true,
nextRunAt = System.currentTimeMillis() + 120_000
@@ -81,14 +81,14 @@ class ClearPredictiveSchedulesTest {
}
val cleared = runBlocking {
ScheduleHelper.clearPredictiveSchedules(context, database)
ScheduleHelper.clearApiSchedules(context, database)
}
assertEquals(1, cleared)
runBlocking {
assertNotNull(database.scheduleDao().getById(dailyReminderId))
assertNull(database.scheduleDao().getById(predictiveId))
assertNull(database.scheduleDao().getById(apiId))
assertNotNull(database.scheduleDao().getById(userScheduleId))
}
}

View File

@@ -32,9 +32,9 @@ public class DailyNotificationPlugin: CAPPlugin {
// Background task identifiers
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
/// Prefix for deterministic UNNotificationRequest identifiers: `predictive_\(Int(timestamp))` with `timestamp` in epoch milliseconds.
private let predictiveNotificationPrefix = "predictive_"
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`predictive_*`).
/// Prefix for deterministic UNNotificationRequest identifiers: `api_\(Int(timestamp))` with `timestamp` in epoch milliseconds.
private let apiNotificationPrefix = "api_"
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`api_*`).
private let dualNotificationRequestIdentifier = "org.timesafari.dailynotification.dual"
/// UserDefaults key for persisted dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
private let dualScheduleConfigKey = "dual_schedule_config"
@@ -430,7 +430,7 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`predictive_*`).
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`api_*`).
@objc func cancelDualSchedule(_ call: CAPPluginCall) {
do {
performCancelDualSchedule()
@@ -1085,7 +1085,7 @@ public class DailyNotificationPlugin: CAPPlugin {
return interval > 0 ? interval : (24 * 60 * 60)
}
/// Epoch milliseconds for the next local occurrence of `hour`:`minute` (used only to build predictive IDs from HH:mm schedules).
/// Epoch milliseconds for the next local occurrence of `hour`:`minute` (used only to build API notification IDs from HH:mm schedules).
private func epochMillisNextDailyOccurrence(hour: Int, minute: Int) -> Int64 {
var comp = DateComponents()
comp.hour = hour
@@ -1097,12 +1097,12 @@ public class DailyNotificationPlugin: CAPPlugin {
return Int64(next.timeIntervalSince1970 * 1000)
}
private func predictiveNotificationId(epochMillis: Int64) -> String {
"\(predictiveNotificationPrefix)\(epochMillis)"
private func apiNotificationId(epochMillis: Int64) -> String {
"\(apiNotificationPrefix)\(epochMillis)"
}
/// Reads persisted millis for a reminder, or derives from stored `time` (HH:mm) for legacy rows.
private func predictiveEpochMillis(from reminder: [String: Any]) -> Int64? {
private func apiEpochMillis(from reminder: [String: Any]) -> Int64? {
if let n = reminder["predictiveEpochMillis"] as? NSNumber {
return n.int64Value
}
@@ -1118,22 +1118,22 @@ public class DailyNotificationPlugin: CAPPlugin {
return epochMillisNextDailyOccurrence(hour: hour, minute: minute)
}
// MARK: - Predictive batch API (replace-all UNUserNotificationCenter)
// MARK: - API batch notifications (replace-all UNUserNotificationCenter)
/// Removes only pending and delivered notifications whose identifiers begin with `predictive_`. Does not touch dual/org IDs or other stacks.
/// Removes only pending and delivered notifications whose identifiers begin with `api_`. Does not touch dual/org IDs or other stacks.
@objc func clearAllNotifications(_ call: CAPPluginCall) {
NSLog("DNP-BATCH: clearAllNotifications — removing predictive_* pending and delivered only")
print("DNP-BATCH: clearAllNotifications — removing predictive_* pending and delivered only")
NSLog("DNP-BATCH: clearAllNotifications — removing api_* pending and delivered only")
print("DNP-BATCH: clearAllNotifications — removing api_* pending and delivered only")
let center = notificationCenter
let prefix = predictiveNotificationPrefix
let prefix = apiNotificationPrefix
let group = DispatchGroup()
group.enter()
center.getPendingNotificationRequests { requests in
let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(prefix) }
center.removePendingNotificationRequests(withIdentifiers: ids)
NSLog("DNP-BATCH: cleared \(ids.count) pending predictive id(s)")
print("DNP-BATCH: cleared \(ids.count) pending predictive id(s)")
NSLog("DNP-BATCH: cleared \(ids.count) pending API notification id(s)")
print("DNP-BATCH: cleared \(ids.count) pending API notification id(s)")
group.leave()
}
@@ -1141,8 +1141,8 @@ public class DailyNotificationPlugin: CAPPlugin {
center.getDeliveredNotifications { notifications in
let ids = notifications.map { $0.request.identifier }.filter { $0.hasPrefix(prefix) }
center.removeDeliveredNotifications(withIdentifiers: ids)
NSLog("DNP-BATCH: cleared \(ids.count) delivered predictive id(s)")
print("DNP-BATCH: cleared \(ids.count) delivered predictive id(s)")
NSLog("DNP-BATCH: cleared \(ids.count) delivered API notification id(s)")
print("DNP-BATCH: cleared \(ids.count) delivered API notification id(s)")
group.leave()
}
@@ -1152,7 +1152,7 @@ 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 predictive notifications using deterministic IDs.
/// Adds/overwrites API-managed notifications using deterministic IDs.
/// Does NOT clear existing notifications. Caller is responsible for lifecycle.
@objc func scheduleNotifications(_ call: CAPPluginCall) {
guard let timestamps = call.getArray("timestamps", Double.self) else {
@@ -1182,7 +1182,7 @@ public class DailyNotificationPlugin: CAPPlugin {
content.title = "Reminder"
content.body = "You have a scheduled notification"
let id = "\(predictiveNotificationPrefix)\(Int(ts))"
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)")
@@ -1262,7 +1262,7 @@ public class DailyNotificationPlugin: CAPPlugin {
)
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
let requestId = predictiveNotificationId(epochMillis: epochMillis)
let requestId = apiNotificationId(epochMillis: epochMillis)
let request = UNNotificationRequest(
identifier: requestId,
@@ -1305,8 +1305,8 @@ public class DailyNotificationPlugin: CAPPlugin {
// Cancel the notification (ID from stored epoch millis)
let reminders = getRemindersFromUserDefaults()
if let stored = reminders.first(where: { ($0["id"] as? String) == reminderId }),
let epochMillis = predictiveEpochMillis(from: stored) {
let requestId = predictiveNotificationId(epochMillis: epochMillis)
let epochMillis = apiEpochMillis(from: stored) {
let requestId = apiNotificationId(epochMillis: epochMillis)
notificationCenter.removePendingNotificationRequests(withIdentifiers: [requestId])
}
@@ -1321,15 +1321,15 @@ public class DailyNotificationPlugin: CAPPlugin {
// Get pending notifications
notificationCenter.getPendingNotificationRequests { requests in
let reminderRequests = requests.filter { $0.identifier.hasPrefix(self.predictiveNotificationPrefix) }
let reminderRequests = requests.filter { $0.identifier.hasPrefix(self.apiNotificationPrefix) }
// Get stored reminder data from UserDefaults
let reminders = self.getRemindersFromUserDefaults()
var result: [[String: Any]] = []
for reminder in reminders {
let expectedId: String? = self.predictiveEpochMillis(from: reminder).map {
self.predictiveNotificationId(epochMillis: $0)
let expectedId: String? = self.apiEpochMillis(from: reminder).map {
self.apiNotificationId(epochMillis: $0)
}
let isScheduled = reminderRequests.contains { expectedId != nil && $0.identifier == expectedId }
var reminderInfo = reminder
@@ -1352,8 +1352,8 @@ public class DailyNotificationPlugin: CAPPlugin {
// Cancel existing reminder (before UserDefaults update)
let remindersBeforeUpdate = getRemindersFromUserDefaults()
if let stored = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId }),
let epochMillis = predictiveEpochMillis(from: stored) {
let oldRequestId = predictiveNotificationId(epochMillis: epochMillis)
let epochMillis = apiEpochMillis(from: stored) {
let oldRequestId = apiNotificationId(epochMillis: epochMillis)
notificationCenter.removePendingNotificationRequests(withIdentifiers: [oldRequestId])
}
@@ -1421,7 +1421,7 @@ public class DailyNotificationPlugin: CAPPlugin {
)
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
let requestId = predictiveNotificationId(epochMillis: epochMillis)
let requestId = apiNotificationId(epochMillis: epochMillis)
let request = UNNotificationRequest(
identifier: requestId,
@@ -1701,7 +1701,7 @@ public class DailyNotificationPlugin: CAPPlugin {
let fireDate = Date().addingTimeInterval(TimeInterval(validSeconds))
let epochMillis = Int64(fireDate.timeIntervalSince1970 * 1000)
let requestId = predictiveNotificationId(epochMillis: epochMillis)
let requestId = apiNotificationId(epochMillis: epochMillis)
let request = UNNotificationRequest(
identifier: requestId,
content: notificationContent,
@@ -2792,7 +2792,7 @@ extension DailyNotificationPlugin {
methods.append(CAPPluginMethod(name: "getScheduledReminders", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "updateDailyReminder", returnType: CAPPluginReturnPromise))
// Predictive batch API (replace-all UNUserNotificationCenter)
// API batch notifications (replace-all UNUserNotificationCenter)
methods.append(CAPPluginMethod(name: "clearAllNotifications", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "scheduleNotifications", returnType: CAPPluginReturnPromise))

View File

@@ -603,12 +603,12 @@ 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.
* Add one-shot API-managed notifications at epoch-ms timestamps (additive).
* Android uses `api_<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
* Android only: cancel API-managed notification schedules (`api_*`) without
* affecting Daily Reminders, user schedules, dual schedules, or fetch jobs.
*/
clearPredictiveNotifications(): Promise<void>;