diff --git a/androidApp/src/main/kotlin/com/avinal/memos/TaskReminderReceiver.kt b/androidApp/src/main/kotlin/com/avinal/memos/TaskReminderReceiver.kt index 38c9bea..d88b052 100644 --- a/androidApp/src/main/kotlin/com/avinal/memos/TaskReminderReceiver.kt +++ b/androidApp/src/main/kotlin/com/avinal/memos/TaskReminderReceiver.kt @@ -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) } } diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt index 8d4991e..e29f654 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt @@ -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 -> diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/NotifyTimeHelper.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/NotifyTimeHelper.kt new file mode 100644 index 0000000..f9666c8 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/NotifyTimeHelper.kt @@ -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() +} diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt index f514574..e00c1e0 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt @@ -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 { val db = Room.databaseBuilder( 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) } } diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt index b84d718..803f081 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/util/PlatformNotification.android.kt @@ -6,12 +6,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -private var liveMemosProvider: (() -> List)? = null +var liveMemosProvider: (() -> List)? = null + private set actual fun setLiveMemosProvider(provider: () -> List) { 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() diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt index 75ab4d7..2ccf814 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt @@ -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() } } } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt index 8659c05..c018382 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/notifications/ReminderScheduler.kt @@ -34,7 +34,6 @@ object ReminderScheduler { tasks: List, nowMillis: Long, timeZone: TimeZone, - alreadyScheduledIds: Set = emptySet(), defaultTime: LocalTime = LocalTime(20, 0), ): List { val alarms = mutableListOf() @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt index d06a5a1..ec74c60 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/PlatformNotification.kt @@ -2,3 +2,4 @@ package com.avinal.memos.util expect fun triggerReminderCheck() expect fun setLiveMemosProvider(provider: () -> List) +expect fun syncNotifyTime(time: String) diff --git a/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt b/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt index 3ad7d64..9fc3728 100644 --- a/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt +++ b/composeApp/src/commonTest/kotlin/com/avinal/memos/ReminderSchedulerTest.kt @@ -29,7 +29,7 @@ class ReminderSchedulerTest { reminder = reminder, priority = priority) private fun compute(tasks: List, 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) diff --git a/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt b/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt index b7fd1c8..eefa3cc 100644 --- a/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/avinal/memos/util/PlatformNotification.ios.kt @@ -2,3 +2,4 @@ package com.avinal.memos.util actual fun triggerReminderCheck() {} actual fun setLiveMemosProvider(provider: () -> List) {} +actual fun syncNotifyTime(time: String) {}