feat(android): add clearPredictiveNotifications for predictive_* schedules only
Lets the app refresh API/predictive notifications without cancelAllNotifications disabling Daily Reminders, dual schedules, or fetch jobs.
This commit is contained in:
@@ -169,6 +169,12 @@ 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.
|
||||
*/
|
||||
const val PREDICTIVE_SCHEDULE_ID_PREFIX = "predictive_"
|
||||
|
||||
// ============================================================
|
||||
// Request Code Versioning
|
||||
// ============================================================
|
||||
|
||||
@@ -743,6 +743,28 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel only API/predictive notification schedules (`predictive_*`).
|
||||
* Does not affect Daily Reminders, user-created schedules, dual schedules, or fetch jobs.
|
||||
*/
|
||||
@PluginMethod
|
||||
fun clearPredictiveNotifications(call: PluginCall) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
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")
|
||||
call.resolve()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to clear predictive notifications", e)
|
||||
call.reject("Failed to clear predictive notifications: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun scheduleDailyReminder(call: PluginCall) {
|
||||
// Alias for scheduleDailyNotification for backward compatibility
|
||||
@@ -3011,6 +3033,50 @@ object ScheduleHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel only predictive/API notification schedules (`predictive_*` 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 {
|
||||
return try {
|
||||
val prefix = DailyNotificationConstants.PREDICTIVE_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")
|
||||
return 0
|
||||
}
|
||||
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
|
||||
predictiveSchedules.forEach { schedule ->
|
||||
NotifyReceiver.cancelNotification(
|
||||
context,
|
||||
scheduleId = schedule.id,
|
||||
triggerAtMillis = schedule.nextRunAt
|
||||
)
|
||||
notificationManager.cancel(schedule.id.hashCode())
|
||||
}
|
||||
|
||||
predictiveSchedules.forEach { database.scheduleDao().deleteById(it.id) }
|
||||
|
||||
database.notificationContentDao().getAllNotifications()
|
||||
.filter { it.id.startsWith(prefix) }
|
||||
.forEach { content ->
|
||||
database.notificationContentDao().deleteNotification(content.id)
|
||||
notificationManager.cancel(content.id.hashCode())
|
||||
}
|
||||
|
||||
Log.i("ScheduleHelper", "clearPredictiveSchedules: cancelled and removed ${predictiveSchedules.size} predictive schedule(s)")
|
||||
predictiveSchedules.size
|
||||
} catch (e: Exception) {
|
||||
Log.e("ScheduleHelper", "clearPredictiveSchedules failed", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.timesafari.dailynotification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28])
|
||||
class ClearPredictiveSchedulesTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var database: DailyNotificationDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
database = TestDBFactory.createInMemoryDatabase(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearPredictiveSchedules_removesOnlyPredictiveAndPreservesDailyReminder() {
|
||||
val dailyReminderId = DailyNotificationConstants.DEFAULT_SCHEDULE_ID
|
||||
val predictiveId = "${DailyNotificationConstants.PREDICTIVE_SCHEDULE_ID_PREFIX}1740000000000"
|
||||
val userScheduleId = "notify_1740000000001"
|
||||
|
||||
runBlocking {
|
||||
database.scheduleDao().upsert(
|
||||
Schedule(
|
||||
id = dailyReminderId,
|
||||
kind = "notify",
|
||||
clockTime = "08:00",
|
||||
enabled = true,
|
||||
nextRunAt = System.currentTimeMillis() + 60_000
|
||||
)
|
||||
)
|
||||
database.scheduleDao().upsert(
|
||||
Schedule(
|
||||
id = predictiveId,
|
||||
kind = "notify",
|
||||
enabled = true,
|
||||
nextRunAt = System.currentTimeMillis() + 120_000
|
||||
)
|
||||
)
|
||||
database.scheduleDao().upsert(
|
||||
Schedule(
|
||||
id = userScheduleId,
|
||||
kind = "notify",
|
||||
enabled = true,
|
||||
nextRunAt = System.currentTimeMillis() + 180_000
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val cleared = runBlocking {
|
||||
ScheduleHelper.clearPredictiveSchedules(context, database)
|
||||
}
|
||||
|
||||
assertEquals(1, cleared)
|
||||
|
||||
runBlocking {
|
||||
assertNotNull(database.scheduleDao().getById(dailyReminderId))
|
||||
assertNull(database.scheduleDao().getById(predictiveId))
|
||||
assertNotNull(database.scheduleDao().getById(userScheduleId))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,6 +602,11 @@ export interface DailyNotificationPlugin {
|
||||
|
||||
getLastNotification(): Promise<NotificationResponse | null>;
|
||||
cancelAllNotifications(): Promise<void>;
|
||||
/**
|
||||
* Android only: cancel API/predictive notification schedules (`predictive_*`) without
|
||||
* affecting Daily Reminders, user schedules, dual schedules, or fetch jobs.
|
||||
*/
|
||||
clearPredictiveNotifications(): Promise<void>;
|
||||
getNotificationStatus(): Promise<NotificationStatus>;
|
||||
updateSettings(settings: NotificationSettings): Promise<void>;
|
||||
getBatteryStatus(): Promise<BatteryStatus>;
|
||||
|
||||
@@ -118,6 +118,10 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async clearPredictiveNotifications(): Promise<void> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async getNotificationStatus(): Promise<{
|
||||
isEnabled?: boolean;
|
||||
isScheduled?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user