1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30

Fix 5 notification reliability issues

1. Remove println debug statement from ReminderScheduler

2. Drop scheduledIds tracking — AlarmManager deduplicates via
   FLAG_UPDATE_CURRENT, and the tracking prevented rescheduling
   when a task's due time was edited

3. TaskCheckWorker uses live app DB via liveMemosProvider when
   available, falls back to opening its own DB only when the
   app process isn't running (boot receiver, background check)

4. Default notify time synced from DataStore to SharedPreferences
   via syncNotifyTime() — both DirectAlarmScheduler and
   TaskCheckWorker now read from the same source via
   readDefaultNotifyTime() helper

5. TaskReminderReceiver triggers runTaskCheckNow() after each
   alarm fires, so the next alarm is scheduled immediately
   instead of waiting up to 15 min for WorkManager

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
2026-06-01 15:24:43 +05:30
parent ef34738591
commit ad536d1e3d
10 changed files with 55 additions and 47 deletions
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.avinal.memos.notifications.TaskNotificationManager
import com.avinal.memos.notifications.runTaskCheckNow
class TaskReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -19,5 +20,6 @@ class TaskReminderReceiver : BroadcastReceiver() {
dueLabel = dueLabel,
priority = priority,
)
runTaskCheckNow(context)
}
}
@@ -7,7 +7,6 @@ import android.content.Intent
import com.avinal.memos.domain.Memo
import com.avinal.memos.parser.TaskParser
import kotlin.time.Clock
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
object DirectAlarmScheduler {
@@ -17,12 +16,9 @@ object DirectAlarmScheduler {
val nowMillis = Clock.System.now().toEpochMilliseconds()
val tz = TimeZone.currentSystemDefault()
val prefs = context.getSharedPreferences("memos_prefs", Context.MODE_PRIVATE)
val defaultTimeStr = prefs.getString("default_notify_time", "20:00") ?: "20:00"
val parts = defaultTimeStr.split(":")
val defaultTime = try { LocalTime(parts[0].toInt(), parts.getOrElse(1) { "0" }.toInt()) } catch (_: Exception) { LocalTime(20, 0) }
val defaultTime = readDefaultNotifyTime(context)
val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, emptySet(), defaultTime)
val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, defaultTime)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarms.forEach { alarm ->
@@ -0,0 +1,20 @@
package com.avinal.memos.notifications
import android.content.Context
import kotlinx.datetime.LocalTime
fun readDefaultNotifyTime(context: Context): LocalTime {
val prefs = context.getSharedPreferences("nikki_notify", Context.MODE_PRIVATE)
val timeStr = prefs.getString("default_notify_time", "20:00") ?: "20:00"
val parts = timeStr.split(":")
return try {
LocalTime(parts[0].toInt(), parts.getOrElse(1) { "0" }.toInt())
} catch (_: Exception) {
LocalTime(20, 0)
}
}
fun writeDefaultNotifyTime(context: Context, time: String) {
context.getSharedPreferences("nikki_notify", Context.MODE_PRIVATE)
.edit().putString("default_notify_time", time).apply()
}
@@ -21,20 +21,26 @@ class TaskCheckWorker(
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val prefs = appContext.getSharedPreferences("task_notifications", Context.MODE_PRIVATE)
val scheduledIds = prefs.getStringSet("scheduled_ids", emptySet()) ?: emptySet()
val nowMillis = Clock.System.now().toEpochMilliseconds()
val defaultTime = readDefaultNotifyTime(appContext)
// Read default notify time from DataStore file (shared prefs fallback)
val notifyPrefs = appContext.getSharedPreferences("memos_prefs", Context.MODE_PRIVATE)
val defaultTimeStr = notifyPrefs.getString("default_notify_time", "20:00") ?: "20:00"
val defaultTimeParts = defaultTimeStr.split(":")
val defaultTime = try {
kotlinx.datetime.LocalTime(defaultTimeParts[0].toInt(), defaultTimeParts.getOrElse(1) { "0" }.toInt())
} catch (_: Exception) {
kotlinx.datetime.LocalTime(20, 0)
val memos = com.avinal.memos.util.liveMemosProvider?.invoke()
?: readMemosFromDb()
val allTasks = memos.flatMap { memo -> TaskParser.extractTasks(memo.id, memo.content, memo.tags) }
val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val tz = TimeZone.currentSystemDefault()
val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, defaultTime)
alarms.forEach { alarm ->
scheduleAlarm(alarmManager, alarm.taskId, alarm.taskText, alarm.label, alarm.triggerAtMillis, alarm.priority)
}
return Result.success()
}
private suspend fun readMemosFromDb(): List<com.avinal.memos.domain.Memo> {
val db = Room.databaseBuilder<MemosDatabase>(
context = appContext,
name = appContext.getDatabasePath("memos.db").absolutePath,
@@ -44,33 +50,11 @@ class TaskCheckWorker(
.setQueryCoroutineContext(Dispatchers.IO)
.build()
try {
val memos = db.memoDao().getAll().map { it.toDomain() }
val allTasks = memos.flatMap { memo -> TaskParser.extractTasks(memo.id, memo.content, memo.tags) }
val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val tz = TimeZone.currentSystemDefault()
val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, scheduledIds, defaultTime)
alarms.forEach { alarm ->
scheduleAlarm(alarmManager, alarm.taskId, alarm.taskText, alarm.label, alarm.triggerAtMillis, alarm.priority)
}
val newScheduledIds = scheduledIds.toMutableSet()
alarms.forEach { newScheduledIds.add(it.taskId) }
val activeTaskIds = allTasks.filter { !it.isCompleted }.map { it.id }.toSet()
val cleaned = newScheduledIds.filter { id ->
val baseId = id.removeSuffix("_am").removeSuffix("_pm").removeSuffix("_remind")
baseId in activeTaskIds
}.toSet()
prefs.edit().putStringSet("scheduled_ids", cleaned).apply()
return try {
db.memoDao().getAll().map { it.toDomain() }
} finally {
db.close()
}
return Result.success()
}
private fun scheduleAlarm(
@@ -103,7 +87,6 @@ class TaskCheckWorker(
try {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
} catch (_: SecurityException) {
// Fallback if exact alarm permission not granted
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
}
}
@@ -6,12 +6,18 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private var liveMemosProvider: (() -> List<com.avinal.memos.domain.Memo>)? = null
var liveMemosProvider: (() -> List<com.avinal.memos.domain.Memo>)? = null
private set
actual fun setLiveMemosProvider(provider: () -> List<com.avinal.memos.domain.Memo>) {
liveMemosProvider = provider
}
actual fun syncNotifyTime(time: String) {
val ctx = appContext ?: return
com.avinal.memos.notifications.writeDefaultNotifyTime(ctx, time)
}
actual fun triggerReminderCheck() {
val ctx = appContext ?: return
val memos = liveMemosProvider?.invoke()
@@ -14,6 +14,7 @@ import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import com.avinal.memos.db.entity.toDomain
import com.avinal.memos.util.syncNotifyTime
class AppDependencies(
dataStorePath: String,
@@ -55,6 +56,7 @@ class AppDependencies(
launch { tokenStore.accessToken.collect { cachedToken = it } }
launch { tokenStore.serverUrl.collect { cachedServerUrl = it } }
launch { tokenStore.syncInterval.collect { memoRepository.syncIntervalMinutes = it } }
launch { tokenStore.defaultNotifyTime.collect { syncNotifyTime(it) } }
launch { initializeLiveMemosProvider() }
}
}
@@ -34,7 +34,6 @@ object ReminderScheduler {
tasks: List<Task>,
nowMillis: Long,
timeZone: TimeZone,
alreadyScheduledIds: Set<String> = emptySet(),
defaultTime: LocalTime = LocalTime(20, 0),
): List<ScheduledAlarm> {
val alarms = mutableListOf<ScheduledAlarm>()
@@ -48,8 +47,6 @@ object ReminderScheduler {
val dueMs = effectiveDate.atTime(effectiveTime).toInstant(timeZone).toEpochMilliseconds()
println("ReminderScheduler: task=${task.text} effectiveDate=$effectiveDate effectiveTime=$effectiveTime dueMs=$dueMs nowMillis=$nowMillis future=${dueMs > nowMillis}")
// Explicit reminder: fire at dueDateTime - duration
if (task.reminder != null) {
val offsetMs = durationToMillis(task.reminder)
@@ -2,3 +2,4 @@ package com.avinal.memos.util
expect fun triggerReminderCheck()
expect fun setLiveMemosProvider(provider: () -> List<com.avinal.memos.domain.Memo>)
expect fun syncNotifyTime(time: String)
@@ -29,7 +29,7 @@ class ReminderSchedulerTest {
reminder = reminder, priority = priority)
private fun compute(tasks: List<Task>, now: Long = nowMillis) =
ReminderScheduler.computeAlarms(tasks, now, tz, emptySet(), defaultTime)
ReminderScheduler.computeAlarms(tasks, now, tz, defaultTime)
@Test fun completedNoAlarms() { assertTrue(compute(listOf(task(completed = true))).isEmpty()) }
@Test fun noDateNoTimeNoAlarms() { assertTrue(compute(listOf(task(date = null, time = null))).isEmpty()) }
@@ -128,7 +128,7 @@ class ReminderSchedulerTest {
@Test fun customDefaultTime() {
val alarms = ReminderScheduler.computeAlarms(
listOf(task()), nowMillis, tz, emptySet(), LocalTime(9, 0)
listOf(task()), nowMillis, tz, LocalTime(9, 0)
)
val expected = dueDate.atTime(LocalTime(9, 0)).toInstant(tz).toEpochMilliseconds()
assertEquals(expected, alarms[0].triggerAtMillis)
@@ -2,3 +2,4 @@ package com.avinal.memos.util
actual fun triggerReminderCheck() {}
actual fun setLiveMemosProvider(provider: () -> List<com.avinal.memos.domain.Memo>) {}
actual fun syncNotifyTime(time: String) {}