refactor(android): P2.1 Batch B - complete cancelAllNotifications() delegation

- Add ScheduleHelper.cancelAlarmsForSchedules() helper method
- Add ScheduleHelper.cancelAllWorkManagerJobs() helper method
- Refactor cancelAllNotifications() to delegate to helpers
- Keep orchestration in plugin (appropriate for multi-service coordination)

Reduces plugin method from ~85 lines to ~45 lines.
Batch B complete: 15 methods refactored to thin adapter pattern.

Refs: docs/progress/P2.1-BATCH-B-STATE.md
This commit is contained in:
Matthew Raymer
2025-12-24 04:18:43 +00:00
parent e604b7f46c
commit ddcafe2a00
3 changed files with 82 additions and 46 deletions

View File

@@ -649,26 +649,11 @@ open class DailyNotificationPlugin : Plugin() {
val schedules = getDatabase().scheduleDao().getAll()
val notifySchedules = schedules.filter { it.kind == "notify" && it.enabled }
// 2. Cancel all AlarmManager alarms (delegate to NotifyReceiver)
var cancelledAlarms = 0
notifySchedules.forEach { schedule ->
try {
// Cancel alarm using the scheduled time (used for request code)
val nextRunAt = schedule.nextRunAt
if (nextRunAt != null && nextRunAt > 0) {
NotifyReceiver.cancelNotification(context, scheduleId = schedule.id, triggerAtMillis = nextRunAt)
cancelledAlarms++
}
} catch (e: Exception) {
// Log but don't fail - alarm might not exist
Log.w(TAG, "Failed to cancel alarm for schedule ${schedule.id}", e)
}
}
// 2. Cancel all AlarmManager alarms (delegate to ScheduleHelper)
// Only cancel alarms we can prove we scheduled (from database)
// Removed unsafe brute-force cancellation loop (was trying request codes 0-100 step 10)
// This prevents accidental cancellation of other alarms and false confidence
// If alarms exist outside the database, they should be tracked or ignored
val cancelledAlarms = ScheduleHelper.cancelAlarmsForSchedules(context, notifySchedules)
if (cancelledAlarms > 0) {
Log.i(TAG, "Cancelled $cancelledAlarms alarm(s) from database schedules")
@@ -676,28 +661,12 @@ open class DailyNotificationPlugin : Plugin() {
Log.d(TAG, "No alarms found in database to cancel")
}
// 3. Cancel all WorkManager jobs
try {
val workManager = WorkManager.getInstance(context)
// Cancel all prefetch jobs
workManager.cancelAllWorkByTag("prefetch")
// Cancel fetch jobs (if using DailyNotificationFetcher tags)
workManager.cancelAllWorkByTag("daily_notification_fetch")
workManager.cancelAllWorkByTag("daily_notification_maintenance")
workManager.cancelAllWorkByTag("soft_refetch")
workManager.cancelAllWorkByTag("daily_notification_display")
workManager.cancelAllWorkByTag("daily_notification_dismiss")
// Cancel unique work by name pattern (prefetch_*)
// Note: WorkManager doesn't support wildcard cancellation, so we cancel by tag
// The unique work names will be replaced when new work is scheduled
// 3. Cancel all WorkManager jobs (delegate to ScheduleHelper)
val workCancelled = ScheduleHelper.cancelAllWorkManagerJobs(context)
if (workCancelled) {
Log.i(TAG, "Cancelled all WorkManager jobs")
} catch (e: Exception) {
Log.w(TAG, "Failed to cancel WorkManager jobs", e)
// Don't fail - continue with database cleanup
} else {
Log.w(TAG, "Failed to cancel some WorkManager jobs, continuing with cleanup")
}
// 4. Clear database state - disable all notification and fetch schedules
@@ -2606,6 +2575,63 @@ object ScheduleHelper {
return enabledSchedules.size
}
/**
* Cancel alarms for a list of schedules
*
* @param context Application context
* @param schedules List of schedules to cancel alarms for
* @return Number of alarms cancelled
*/
suspend fun cancelAlarmsForSchedules(
context: Context,
schedules: List<Schedule>
): Int {
var cancelledCount = 0
schedules.forEach { schedule ->
try {
val nextRunAt = schedule.nextRunAt
if (nextRunAt != null && nextRunAt > 0) {
NotifyReceiver.cancelNotification(context, scheduleId = schedule.id, triggerAtMillis = nextRunAt)
cancelledCount++
}
} catch (e: Exception) {
// Log but don't fail - alarm might not exist
Log.w("ScheduleHelper", "Failed to cancel alarm for schedule ${schedule.id}", e)
}
}
return cancelledCount
}
/**
* Cancel all WorkManager jobs by tags
*
* @param context Application context
* @return true if cancellation was successful
*/
suspend fun cancelAllWorkManagerJobs(context: Context): Boolean {
return try {
val workManager = WorkManager.getInstance(context)
// Cancel all prefetch jobs
workManager.cancelAllWorkByTag("prefetch")
// Cancel fetch jobs (if using DailyNotificationFetcher tags)
workManager.cancelAllWorkByTag("daily_notification_fetch")
workManager.cancelAllWorkByTag("daily_notification_maintenance")
workManager.cancelAllWorkByTag("soft_refetch")
workManager.cancelAllWorkByTag("daily_notification_display")
workManager.cancelAllWorkByTag("daily_notification_dismiss")
// Note: WorkManager doesn't support wildcard cancellation, so we cancel by tag
// The unique work names will be replaced when new work is scheduled
true
} catch (e: Exception) {
Log.w("ScheduleHelper", "Failed to cancel WorkManager jobs", e)
false
}
}
/**
* Clean up existing notification schedules (cancel alarms and delete from database)
* Used to ensure "one per day" semantics for daily notifications