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

Fix notification scheduling, custom reminder picker, 125 tests

Notification fixes:
- Removed scheduledIds skip — alarms always recomputed on each worker
  run (AlarmManager deduplicates via FLAG_UPDATE_CURRENT)
- Tasks with dueTime but no dueDate now assume today
- Worker runs once immediately on app launch + periodic 15min
- Added debug logging to trace task/alarm computation
- "check reminders now" button in settings triggers immediate check

Custom reminder picker:
- Number input field (digits only) + unit selector (min/hr/day/week)
- Replaces the preset-only list with free-form input
- "save" validates > 0 before applying, "clear" removes reminder

ReminderScheduler extracted to commonMain (testable):
- 18 tests covering: completed/no-date skip, due time alarms,
  default 8am/8pm, duration offsets (min/hr/day/week),
  past alarm filtering, multiple tasks, label preservation

125 tests total, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context)

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
2026-05-21 20:00:48 +05:30
parent 9bebc628bd
commit 8c3ab59f2f
9 changed files with 366 additions and 54 deletions
@@ -13,12 +13,7 @@ import com.avinal.memos.db.entity.toDomain
import com.avinal.memos.parser.TaskParser import com.avinal.memos.parser.TaskParser
import kotlin.time.Clock import kotlin.time.Clock
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.todayIn
class TaskCheckWorker( class TaskCheckWorker(
private val appContext: Context, private val appContext: Context,
@@ -27,11 +22,7 @@ class TaskCheckWorker(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val prefs = appContext.getSharedPreferences("task_notifications", Context.MODE_PRIVATE) val prefs = appContext.getSharedPreferences("task_notifications", Context.MODE_PRIVATE)
val notificationsEnabled = prefs.getBoolean("enabled", true)
if (!notificationsEnabled) return Result.success()
val scheduledIds = prefs.getStringSet("scheduled_ids", emptySet()) ?: emptySet() val scheduledIds = prefs.getStringSet("scheduled_ids", emptySet()) ?: emptySet()
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val nowMillis = Clock.System.now().toEpochMilliseconds() val nowMillis = Clock.System.now().toEpochMilliseconds()
val db = Room.databaseBuilder<MemosDatabase>( val db = Room.databaseBuilder<MemosDatabase>(
@@ -48,51 +39,26 @@ class TaskCheckWorker(
val allTasks = memos.flatMap { memo -> TaskParser.extractTasks(memo.id, memo.content) } val allTasks = memos.flatMap { memo -> TaskParser.extractTasks(memo.id, memo.content) }
val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val tz = TimeZone.currentSystemDefault() val tz = TimeZone.currentSystemDefault()
val newScheduledIds = scheduledIds.toMutableSet()
allTasks.forEach { task -> android.util.Log.d("TaskCheckWorker", "Found ${memos.size} memos, ${allTasks.size} tasks, scheduled=${scheduledIds.size}")
if (task.isCompleted || task.dueDate == null || task.id in scheduledIds) return@forEach allTasks.forEach { t ->
android.util.Log.d("TaskCheckWorker", "Task: ${t.text}, date=${t.dueDate}, time=${t.dueTime}, reminder=${t.reminder}, completed=${t.isCompleted}, id=${t.id}")
// Schedule reminder as duration before due
if (task.reminder != null) {
val dueInstant = task.dueDate.atTime(task.dueTime ?: LocalTime(8, 0)).toInstant(tz)
val offsetMs = when (task.reminder.unit) {
com.avinal.memos.domain.ReminderUnit.MIN -> task.reminder.value * 60_000L
com.avinal.memos.domain.ReminderUnit.HR -> task.reminder.value * 3_600_000L
com.avinal.memos.domain.ReminderUnit.DAY -> task.reminder.value * 86_400_000L
com.avinal.memos.domain.ReminderUnit.WEEK -> task.reminder.value * 604_800_000L
}
val reminderMs = dueInstant.toEpochMilliseconds() - offsetMs
if (reminderMs > nowMillis) {
scheduleAlarm(alarmManager, "${task.id}_remind", task.text, "reminder: ${task.reminder}", reminderMs)
}
}
if (task.dueTime != null) {
val alarmInstant = task.dueDate.atTime(task.dueTime).toInstant(tz)
val alarmMs = alarmInstant.toEpochMilliseconds()
if (alarmMs > nowMillis) {
scheduleAlarm(alarmManager, task.id, task.text, "due at ${task.dueTime}", alarmMs)
newScheduledIds.add(task.id)
}
} else {
val morning = task.dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds()
val evening = task.dueDate.atTime(LocalTime(20, 0)).toInstant(tz).toEpochMilliseconds()
if (morning > nowMillis) {
scheduleAlarm(alarmManager, "${task.id}_am", task.text, "due today", morning)
}
if (evening > nowMillis) {
scheduleAlarm(alarmManager, "${task.id}_pm", task.text, "reminder: still due today", evening)
}
newScheduledIds.add(task.id)
}
} }
// Clean up IDs for completed/removed tasks val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, scheduledIds)
android.util.Log.d("TaskCheckWorker", "Computed ${alarms.size} alarms")
alarms.forEach { alarm ->
android.util.Log.d("TaskCheckWorker", "Scheduling: ${alarm.taskText} at ${alarm.triggerAtMillis} (${alarm.label})")
scheduleAlarm(alarmManager, alarm.taskId, alarm.taskText, alarm.label, alarm.triggerAtMillis)
}
val newScheduledIds = scheduledIds.toMutableSet()
alarms.forEach { newScheduledIds.add(it.taskId) }
val activeTaskIds = allTasks.filter { !it.isCompleted }.map { it.id }.toSet() val activeTaskIds = allTasks.filter { !it.isCompleted }.map { it.id }.toSet()
val cleaned = newScheduledIds.filter { id -> val cleaned = newScheduledIds.filter { id ->
val baseId = id.removeSuffix("_am").removeSuffix("_pm") val baseId = id.removeSuffix("_am").removeSuffix("_pm").removeSuffix("_remind")
baseId in activeTaskIds baseId in activeTaskIds
}.toSet() }.toSet()
@@ -2,11 +2,24 @@ package com.avinal.memos.notifications
import android.content.Context import android.content.Context
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
fun runTaskCheckNow(context: Context) {
WorkManager.getInstance(context).enqueue(
OneTimeWorkRequestBuilder<TaskCheckWorker>().build()
)
}
fun scheduleTaskChecker(context: Context) { fun scheduleTaskChecker(context: Context) {
// Run once immediately on app launch
WorkManager.getInstance(context).enqueue(
OneTimeWorkRequestBuilder<TaskCheckWorker>().build()
)
// Then every 15 minutes
val request = PeriodicWorkRequestBuilder<TaskCheckWorker>(15, TimeUnit.MINUTES).build() val request = PeriodicWorkRequestBuilder<TaskCheckWorker>(15, TimeUnit.MINUTES).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork( WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"task_check", "task_check",
@@ -0,0 +1,8 @@
package com.avinal.memos.util
import com.avinal.memos.notifications.runTaskCheckNow
actual fun triggerReminderCheck() {
val ctx = appContext ?: return
runTaskCheckNow(ctx)
}
@@ -0,0 +1,70 @@
package com.avinal.memos.notifications
import com.avinal.memos.domain.ReminderUnit
import com.avinal.memos.domain.Task
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.todayIn
data class ScheduledAlarm(
val taskId: String,
val taskText: String,
val label: String,
val triggerAtMillis: Long,
)
object ReminderScheduler {
fun computeAlarms(
tasks: List<Task>,
nowMillis: Long,
timeZone: TimeZone,
alreadyScheduledIds: Set<String>,
): List<ScheduledAlarm> {
val alarms = mutableListOf<ScheduledAlarm>()
val today = kotlin.time.Clock.System.todayIn(timeZone)
tasks.forEach { task ->
if (task.isCompleted) return@forEach
val effectiveDate = task.dueDate ?: if (task.dueTime != null) today else return@forEach
// Explicit reminder: fire at dueDateTime - duration
if (task.reminder != null) {
val dueInstant = effectiveDate.atTime(task.dueTime ?: LocalTime(8, 0)).toInstant(timeZone)
val offsetMs = when (task.reminder.unit) {
ReminderUnit.MIN -> task.reminder.value * 60_000L
ReminderUnit.HR -> task.reminder.value * 3_600_000L
ReminderUnit.DAY -> task.reminder.value * 86_400_000L
ReminderUnit.WEEK -> task.reminder.value * 604_800_000L
}
val reminderMs = dueInstant.toEpochMilliseconds() - offsetMs
if (reminderMs > nowMillis) {
alarms.add(ScheduledAlarm("${task.id}_remind", task.text, "reminder: ${task.reminder}", reminderMs))
}
}
// Due time alarm
if (task.dueTime != null) {
val alarmMs = effectiveDate.atTime(task.dueTime).toInstant(timeZone).toEpochMilliseconds()
if (alarmMs > nowMillis) {
alarms.add(ScheduledAlarm(task.id, task.text, "due at ${task.dueTime}", alarmMs))
}
} else {
// Default: 8am and 8pm on due date
val morning = effectiveDate.atTime(LocalTime(8, 0)).toInstant(timeZone).toEpochMilliseconds()
val evening = effectiveDate.atTime(LocalTime(20, 0)).toInstant(timeZone).toEpochMilliseconds()
if (morning > nowMillis) {
alarms.add(ScheduledAlarm("${task.id}_am", task.text, "due today", morning))
}
if (evening > nowMillis) {
alarms.add(ScheduledAlarm("${task.id}_pm", task.text, "reminder: still due today", evening))
}
}
}
return alarms
}
}
@@ -165,6 +165,13 @@ fun SettingsScreen(
) )
} }
Text("get notified when tasks are due or overdue", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text("get notified when tasks are due or overdue", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
"check reminders now",
fontSize = 13.sp, color = accent,
modifier = Modifier.clickable {
com.avinal.memos.util.triggerReminderCheck()
}.padding(vertical = 4.dp),
)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
SectionHeader("backup") SectionHeader("backup")
@@ -92,11 +92,52 @@ fun TaskDetailSheet(
} }
if (showReminderPicker) { if (showReminderPicker) {
val options = listOf(ReminderDuration(15, ReminderUnit.MIN), ReminderDuration(30, ReminderUnit.MIN), ReminderDuration(1, ReminderUnit.HR), ReminderDuration(2, ReminderUnit.HR), ReminderDuration(1, ReminderUnit.DAY), ReminderDuration(2, ReminderUnit.DAY), ReminderDuration(1, ReminderUnit.WEEK)) var reminderValue by remember { mutableStateOf(currentTask.reminder?.value?.toString() ?: "30") }
AlertDialog(onDismissRequest = { showReminderPicker = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null, var reminderUnit by remember { mutableStateOf(currentTask.reminder?.unit ?: ReminderUnit.MIN) }
text = { Column { options.forEach { d -> Text(d.toString(), fontSize = 15.sp, color = if (currentTask.reminder == d) accent else textColor, fontWeight = if (currentTask.reminder == d) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.fillMaxWidth().clickable { doUpdate(currentTask.copy(reminder = d)); showReminderPicker = false }.padding(vertical = 8.dp)) AlertDialog(
}; if (currentTask.reminder != null) { Text("no reminder", fontSize = 15.sp, color = subtleColor, modifier = Modifier.fillMaxWidth().clickable { doUpdate(currentTask.copy(reminder = null)); showReminderPicker = false }.padding(vertical = 8.dp)) } } }, confirmButton = {}) onDismissRequest = { showReminderPicker = false },
containerColor = MaterialTheme.colorScheme.surfaceContainer,
title = null,
text = {
Column {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextField(
value = reminderValue,
onValueChange = { reminderValue = it.filter { c -> c.isDigit() } },
modifier = Modifier.width(72.dp),
singleLine = true,
textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor),
colors = TextFieldDefaults.colors(focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedIndicatorColor = accent, unfocusedIndicatorColor = subtleColor.copy(alpha = 0.3f), cursorColor = accent),
)
ReminderUnit.entries.forEach { unit ->
val sel = reminderUnit == unit
Text(
unit.suffix, fontSize = 14.sp,
color = if (sel) accent else textColor,
fontWeight = if (sel) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.clickable { reminderUnit = unit }.padding(horizontal = 6.dp, vertical = 4.dp),
)
}
}
Spacer(Modifier.height(12.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
if (currentTask.reminder != null) {
Text("clear", fontSize = 14.sp, color = subtleColor, modifier = Modifier.clickable { showReminderPicker = false; doUpdate(currentTask.copy(reminder = null)) })
} else {
Spacer(Modifier.width(1.dp))
}
Text("save", fontSize = 14.sp, color = accent, fontWeight = FontWeight.SemiBold,
modifier = Modifier.clickable {
showReminderPicker = false
val v = reminderValue.toIntOrNull()
if (v != null && v > 0) doUpdate(currentTask.copy(reminder = ReminderDuration(v, reminderUnit)))
})
}
}
},
confirmButton = {},
)
} }
if (showPriorityPicker) { if (showPriorityPicker) {
@@ -0,0 +1,3 @@
package com.avinal.memos.util
expect fun triggerReminderCheck()
@@ -0,0 +1,199 @@
package com.avinal.memos
import com.avinal.memos.domain.ReminderDuration
import com.avinal.memos.domain.ReminderUnit
import com.avinal.memos.domain.Task
import com.avinal.memos.notifications.ReminderScheduler
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ReminderSchedulerTest {
private val tz = TimeZone.UTC
private val dueDate = LocalDate(2026, 6, 15)
// nowMillis = 2026-06-15 00:00 UTC
private val nowMillis = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds()
private fun task(
id: String = "t1",
completed: Boolean = false,
date: LocalDate? = dueDate,
time: LocalTime? = null,
reminder: ReminderDuration? = null,
priority: Int? = null,
) = Task(
id = id, memoId = "m1", lineIndex = 0, text = "Test",
isCompleted = completed, dueDate = date, dueTime = time,
reminder = reminder, priority = priority,
)
@Test
fun completedTaskProducesNoAlarms() {
val alarms = ReminderScheduler.computeAlarms(listOf(task(completed = true)), nowMillis, tz, emptySet())
assertTrue(alarms.isEmpty())
}
@Test
fun taskWithNoDateProducesNoAlarms() {
val alarms = ReminderScheduler.computeAlarms(listOf(task(date = null)), nowMillis, tz, emptySet())
assertTrue(alarms.isEmpty())
}
@Test
fun alreadyScheduledTaskStillRecomputed() {
// Alarms are always recomputed — AlarmManager deduplicates via PendingIntent
val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, setOf("t1"))
assertTrue(alarms.isNotEmpty())
}
@Test
fun taskWithDueTimeProducesOneAlarm() {
val t = task(time = LocalTime(15, 0))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
assertEquals(1, alarms.size)
assertEquals("t1", alarms[0].taskId)
assertTrue(alarms[0].label.contains("due at"))
}
@Test
fun taskWithDueTimeAlarmAtCorrectTime() {
val t = task(time = LocalTime(15, 0))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
val expected = dueDate.atTime(LocalTime(15, 0)).toInstant(tz).toEpochMilliseconds()
assertEquals(expected, alarms[0].triggerAtMillis)
}
@Test
fun taskWithoutTimeProducesTwoAlarms() {
val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet())
assertEquals(2, alarms.size)
assertTrue(alarms.any { it.taskId == "t1_am" })
assertTrue(alarms.any { it.taskId == "t1_pm" })
}
@Test
fun defaultAlarmsAt8amAnd8pm() {
val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet())
val am = alarms.first { it.taskId == "t1_am" }
val pm = alarms.first { it.taskId == "t1_pm" }
val expected8am = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds()
val expected8pm = dueDate.atTime(LocalTime(20, 0)).toInstant(tz).toEpochMilliseconds()
assertEquals(expected8am, am.triggerAtMillis)
assertEquals(expected8pm, pm.triggerAtMillis)
}
@Test
fun reminderDurationOffset30min() {
val t = task(time = LocalTime(15, 0), reminder = ReminderDuration(30, ReminderUnit.MIN))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
val reminder = alarms.first { it.taskId.endsWith("_remind") }
val expected = dueDate.atTime(LocalTime(15, 0)).toInstant(tz).toEpochMilliseconds() - 30 * 60_000L
assertEquals(expected, reminder.triggerAtMillis)
}
@Test
fun reminderDurationOffset1hr() {
val t = task(time = LocalTime(10, 0), reminder = ReminderDuration(1, ReminderUnit.HR))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
val reminder = alarms.first { it.taskId.endsWith("_remind") }
val expected = dueDate.atTime(LocalTime(10, 0)).toInstant(tz).toEpochMilliseconds() - 3_600_000L
assertEquals(expected, reminder.triggerAtMillis)
}
@Test
fun reminderDurationOffset1day() {
val t = task(reminder = ReminderDuration(1, ReminderUnit.DAY))
// Set now to 2 days before due date so the 1-day reminder is in the future
val earlyNow = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds() - 2 * 86_400_000L
val alarms = ReminderScheduler.computeAlarms(listOf(t), earlyNow, tz, emptySet())
val reminder = alarms.first { it.taskId.endsWith("_remind") }
val expected = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() - 86_400_000L
assertEquals(expected, reminder.triggerAtMillis)
}
@Test
fun reminderDurationOffset1week() {
val t = task(reminder = ReminderDuration(1, ReminderUnit.WEEK))
val earlyNow = dueDate.atTime(LocalTime(0, 0)).toInstant(tz).toEpochMilliseconds() - 8 * 86_400_000L
val alarms = ReminderScheduler.computeAlarms(listOf(t), earlyNow, tz, emptySet())
val reminder = alarms.first { it.taskId.endsWith("_remind") }
val expected = dueDate.atTime(LocalTime(8, 0)).toInstant(tz).toEpochMilliseconds() - 604_800_000L
assertEquals(expected, reminder.triggerAtMillis)
}
@Test
fun reminderWithDueTimeProducesBothAlarms() {
val t = task(time = LocalTime(14, 0), reminder = ReminderDuration(30, ReminderUnit.MIN))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
// Should have: reminder alarm + due time alarm
assertEquals(2, alarms.size)
assertTrue(alarms.any { it.taskId.endsWith("_remind") })
assertTrue(alarms.any { it.taskId == "t1" })
}
@Test
fun pastAlarmsNotScheduled() {
// now is after the due time
val lateNow = dueDate.atTime(LocalTime(23, 0)).toInstant(tz).toEpochMilliseconds()
val t = task(time = LocalTime(10, 0))
val alarms = ReminderScheduler.computeAlarms(listOf(t), lateNow, tz, emptySet())
assertTrue(alarms.isEmpty())
}
@Test
fun pastReminderNotScheduledButDueTimeStillIs() {
// now is after the reminder time but before due time
val t = task(time = LocalTime(15, 0), reminder = ReminderDuration(2, ReminderUnit.HR))
val midNow = dueDate.atTime(LocalTime(14, 0)).toInstant(tz).toEpochMilliseconds()
val alarms = ReminderScheduler.computeAlarms(listOf(t), midNow, tz, emptySet())
// Reminder at 13:00 is past, due at 15:00 is future
assertEquals(1, alarms.size)
assertEquals("t1", alarms[0].taskId)
}
@Test
fun multipleTasksProduceCorrectAlarms() {
val tasks = listOf(
task(id = "a", time = LocalTime(9, 0)),
task(id = "b", time = LocalTime(17, 0)),
task(id = "c", completed = true),
task(id = "d", date = null),
)
val alarms = ReminderScheduler.computeAlarms(tasks, nowMillis, tz, emptySet())
assertEquals(2, alarms.size)
assertTrue(alarms.any { it.taskId == "a" })
assertTrue(alarms.any { it.taskId == "b" })
}
@Test
fun alarmLabelsCorrect() {
val t = task(time = LocalTime(14, 0), reminder = ReminderDuration(30, ReminderUnit.MIN))
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
val reminder = alarms.first { it.taskId.endsWith("_remind") }
assertTrue(reminder.label.contains("reminder"))
val due = alarms.first { it.taskId == "t1" }
assertTrue(due.label.contains("due at"))
}
@Test
fun defaultAlarmLabelsCorrect() {
val alarms = ReminderScheduler.computeAlarms(listOf(task()), nowMillis, tz, emptySet())
val am = alarms.first { it.taskId == "t1_am" }
val pm = alarms.first { it.taskId == "t1_pm" }
assertEquals("due today", am.label)
assertTrue(pm.label.contains("still due"))
}
@Test
fun taskTextPreservedInAlarm() {
val t = task().copy(text = "Buy groceries")
val alarms = ReminderScheduler.computeAlarms(listOf(t), nowMillis, tz, emptySet())
assertTrue(alarms.all { it.taskText == "Buy groceries" })
}
}
@@ -0,0 +1,5 @@
package com.avinal.memos.util
actual fun triggerReminderCheck() {
// TODO: iOS notification check
}