Convert random and snooze reminders to alarms

Display snooze time in edit screen
pull/1769/head
Alex Baker 4 years ago
parent 67899e6fff
commit cb834a9818

@ -1,24 +1,21 @@
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE_TIME
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.Alarm.Companion.whenDue import org.tasks.data.Alarm.Companion.whenDue
import org.tasks.data.Alarm.Companion.whenOverdue import org.tasks.data.Alarm.Companion.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted
import org.tasks.data.AlarmDao import org.tasks.data.AlarmDao
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.jobs.AlarmEntry import org.tasks.jobs.AlarmEntry
@ -27,11 +24,9 @@ import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_DATE import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.DUE_TIME import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.REMINDER_LAST import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -45,56 +40,20 @@ class AlarmJobServiceTest : InjectingTestCase() {
@Test @Test
fun scheduleAlarm() = runBlocking { fun scheduleAlarm() = runBlocking {
val task = taskDao.createNew(newTask()) val task = taskDao.createNew(newTask())
val alarm = insertAlarm(Alarm(task, DateTime(2017, 9, 24, 19, 57).millis)) val alarm = insertAlarm(Alarm(task, DateTime(2017, 9, 24, 19, 57).millis, TYPE_DATE_TIME))
verify(AlarmEntry(alarm, task, DateTime(2017, 9, 24, 19, 57).millis)) verify(AlarmEntry(alarm, task, DateTime(2017, 9, 24, 19, 57).millis, TYPE_DATE_TIME))
} }
@Test @Test
fun ignoreStaleAlarm() = runBlocking { fun ignoreStaleAlarm() = runBlocking {
val alarmTime = DateTime(2017, 9, 24, 19, 57) val alarmTime = DateTime(2017, 9, 24, 19, 57)
val task = taskDao.createNew(newTask(with(REMINDER_LAST, alarmTime.endOfMinute()))) val task = taskDao.createNew(newTask(with(REMINDER_LAST, alarmTime.endOfMinute())))
alarmDao.insert(Alarm(task, alarmTime.millis)) alarmDao.insert(Alarm(task, alarmTime.millis, TYPE_DATE_TIME))
verify() verify()
} }
@Test
fun scheduleReminderAtDefaultDue() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_DATE, now)))
val alarm = alarmDao.insert(whenDue(task))
verify(AlarmEntry(alarm, task, now.startOfDay().withHourOfDay(18).millis))
}
@Test
fun scheduleReminderAtDefaultDueTime() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, now)))
val alarm = alarmDao.insert(whenDue(task))
verify(AlarmEntry(alarm, task, now.startOfMinute().millis + 1000))
}
@Test
fun scheduleReminderAtDefaultStart() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_DATE, now), with(HIDE_TYPE, HIDE_UNTIL_DUE)))
val alarm = alarmDao.insert(whenStarted(task))
verify(AlarmEntry(alarm, task, now.startOfDay().withHourOfDay(18).millis))
}
@Test
fun scheduleReminerAtDefaultStartTime() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, now), with(HIDE_TYPE, HIDE_UNTIL_DUE_TIME)))
val alarm = alarmDao.insert(whenStarted(task))
verify(AlarmEntry(alarm, task, now.startOfMinute().millis + 1000))
}
@Test @Test
fun dontScheduleReminderForCompletedTask() = runBlocking { fun dontScheduleReminderForCompletedTask() = runBlocking {
val task = taskDao.insert( val task = taskDao.insert(
@ -122,107 +81,16 @@ class AlarmJobServiceTest : InjectingTestCase() {
} }
@Test @Test
fun scheduleRelativeAfterDue() = runBlocking { fun snoozeOverridesAll() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_DATE, now)))
val alarm = alarmDao.insert(Alarm(task, TimeUnit.DAYS.toMillis(1), Alarm.TYPE_REL_END))
verify(AlarmEntry(alarm, task, now.plusDays(1).startOfDay().withHourOfDay(18).millis))
}
@Test
fun scheduleRelativeAfterDueTime() = runBlocking {
val now = newDateTime() val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, now))) val task = taskDao.insert(newTask(with(DUE_TIME, now)))
val alarm = alarmDao.insert(Alarm(task, TimeUnit.DAYS.toMillis(1), Alarm.TYPE_REL_END))
verify(AlarmEntry(alarm, task, now.plusDays(1).startOfMinute().millis + 1000))
}
@Test
fun scheduleRelativeAfterStart() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_DATE, now), with(HIDE_TYPE, HIDE_UNTIL_DUE)))
val alarm = alarmDao.insert(Alarm(task, TimeUnit.DAYS.toMillis(1), Alarm.TYPE_REL_START))
verify(AlarmEntry(alarm, task, now.plusDays(1).startOfDay().withHourOfDay(18).millis))
}
@Test
fun scheduleRelativeAfterStartTime() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, now), with(HIDE_TYPE, HIDE_UNTIL_DUE_TIME)))
val alarm = alarmDao.insert(Alarm(task, TimeUnit.DAYS.toMillis(1), Alarm.TYPE_REL_START))
verify(AlarmEntry(alarm, task, now.plusDays(1).startOfMinute().millis + 1000))
}
@Test
fun scheduleFirstRepeatReminder() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(4)))
)
val alarm = alarmDao.insert(Alarm(task, 0, Alarm.TYPE_REL_END, 1, TimeUnit.MINUTES.toMillis(5)))
verify(AlarmEntry(alarm, task, now.plusMinutes(5).startOfMinute().millis + 1000))
}
@Test
fun scheduleSecondRepeatReminder() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(6)))
)
val alarm = alarmDao.insert(Alarm(task, 0, Alarm.TYPE_REL_END, 2, TimeUnit.MINUTES.toMillis(5)))
verify(AlarmEntry(alarm, task, now.plusMinutes(10).startOfMinute().millis + 1000))
}
@Test
fun terminateRepeatReminder() = runBlocking {
val now = Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, now()).toDateTime()
val task = taskDao.insert(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(10)))
)
alarmDao.insert(Alarm(task, 0, Alarm.TYPE_REL_END, 2, TimeUnit.MINUTES.toMillis(5)))
verify()
}
@Test
fun dontScheduleRelativeEndWithNoEnd() = runBlocking {
val task = taskDao.insert(newTask())
alarmDao.insert(whenDue(task)) alarmDao.insert(whenDue(task))
verify()
}
@Test
fun dontScheduleRelativeStartWithNoStart() = runBlocking {
val now = newDateTime()
val task = taskDao.insert(newTask(with(DUE_DATE, now)))
alarmDao.insert(whenStarted(task))
verify()
}
@Test
fun reminderOverdueEveryDay() = runBlocking {
val dueDate = Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, DateTime(2022, 1, 30, 13, 30).millis).toDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, dueDate), with(REMINDER_LAST, dueDate.plusDays(6))))
val alarm = alarmDao.insert(whenOverdue(task))
verify(AlarmEntry(alarm, task, dueDate.plusDays(7).millis))
}
@Test
fun endDailyOverdueReminder() = runBlocking {
val dueDate = Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, DateTime(2022, 1, 30, 13, 30).millis).toDateTime()
val task = taskDao.insert(newTask(with(DUE_TIME, dueDate), with(REMINDER_LAST, dueDate.plusDays(7))))
alarmDao.insert(whenOverdue(task)) alarmDao.insert(whenOverdue(task))
alarmDao.insert(Alarm(task, DateUtilities.ONE_HOUR, TYPE_RANDOM))
val alarm = alarmDao.insert(Alarm(task, now.plusMonths(12).millis, TYPE_SNOOZE))
verify() verify(AlarmEntry(alarm, task, now.plusMonths(12).millis, TYPE_SNOOZE))
} }
private suspend fun insertAlarm(alarm: Alarm): Long { private suspend fun insertAlarm(alarm: Alarm): Long {

@ -1,156 +0,0 @@
package com.todoroo.astrid.reminders
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.Freeze.Companion.freezeClock
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD
import org.tasks.makers.TaskMaker.REMINDERS
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import org.tasks.reminders.Random
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ReminderServiceTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
private lateinit var service: ReminderService
private lateinit var random: RandomStub
@Before
override fun setUp() {
super.setUp()
random = RandomStub()
preferences.clear()
service = ReminderService(jobs, random, taskDao)
}
@Test
fun ignoreStaleSnoozeTime() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime()),
with(SNOOZE_TIME, newDateTime().minusMinutes(5)),
with(REMINDER_LAST, newDateTime().minusMinutes(4))
)
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontIgnoreMissedSnoozeTime() {
val dueDate = newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(SNOOZE_TIME, dueDate.minusMinutes(4)),
with(REMINDER_LAST, dueDate.minusMinutes(5)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.reminderSnooze, ReminderService.TYPE_SNOOZE))
}
@Test
fun scheduleInitialRandomReminder() {
random.seed = 0.3865f
freezeClock {
val now = newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, null as DateTime?),
with(CREATION_TIME, now.minusDays(1)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleNextRandomReminder() {
random.seed = 0.3865f
freezeClock {
val now = newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(1)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleOverdueRandomReminder() {
random.seed = 0.3865f
freezeClock {
val now = newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(14)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.millis + 10148400, ReminderService.TYPE_RANDOM))
}
}
@Test
fun snoozeOverridesAll() {
val now = newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(SNOOZE_TIME, now.plusMonths(12)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE or Task.NOTIFY_AFTER_DEADLINE),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_HOUR))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.plusMonths(12).millis, ReminderService.TYPE_SNOOZE))
}
private fun verify(vararg reminders: ReminderEntry) = assertEquals(reminders.toList(), jobs.getJobs())
internal class RandomStub : Random() {
var seed = 1.0f
override fun nextFloat() = seed
}
}

@ -11,6 +11,7 @@ import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@ -27,7 +28,6 @@ import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@ -36,6 +36,7 @@ import javax.inject.Inject
class LocationDaoTest : InjectingTestCase() { class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao @Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmDao: AlarmDao
@Test @Test
fun getExistingPlace() = runBlocking { fun getExistingPlace() = runBlocking {
@ -123,8 +124,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter { freezeAt(now()).thawAfter {
val place = newPlace() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15)))) val task = taskDao.createNew(newTask())
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))) alarmDao.insert(Alarm(task, newDateTime().plusMinutes(15).millis, TYPE_SNOOZE))
locationDao.insert(newGeofence(with(TASK, task), with(PLACE, place.uid), with(ARRIVAL, true)))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty())
} }
@ -135,8 +137,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter { freezeAt(now()).thawAfter {
val place = newPlace() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15)))) val task = taskDao.createNew(newTask())
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))) alarmDao.insert(Alarm(task, newDateTime().plusMinutes(15).millis, TYPE_SNOOZE))
locationDao.insert(newGeofence(with(TASK, task), with(PLACE, place.uid), with(DEPARTURE, true)))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty())
} }
@ -147,8 +150,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter { freezeAt(now()).thawAfter {
val place = newPlace() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15)))) val task = taskDao.createNew(newTask())
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)) alarmDao.insert(Alarm(task, newDateTime().minusMinutes(15).millis, TYPE_SNOOZE))
val geofence = newGeofence(with(TASK, task), with(PLACE, place.uid), with(ARRIVAL, true))
geofence.id = locationDao.insert(geofence) geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
@ -160,8 +164,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter { freezeAt(now()).thawAfter {
val place = newPlace() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15)))) val task = taskDao.createNew(newTask())
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)) alarmDao.insert(Alarm(task, newDateTime().minusMinutes(15).millis, TYPE_SNOOZE))
val geofence = newGeofence(with(TASK, task), with(PLACE, place.uid), with(DEPARTURE, true))
geofence.id = locationDao.insert(geofence) geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now()))

@ -13,6 +13,9 @@ import org.tasks.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.caldav.iCalendar.Companion.snooze import org.tasks.caldav.iCalendar.Companion.snooze
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.AlarmDao
import org.tasks.data.TagDao import org.tasks.data.TagDao
import org.tasks.data.TagDataDao import org.tasks.data.TagDataDao
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@ -28,7 +31,6 @@ import org.tasks.makers.TagMaker.TASK
import org.tasks.makers.TagMaker.newTag import org.tasks.makers.TagMaker.newTag
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.COLLAPSED import org.tasks.makers.TaskMaker.COLLAPSED
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.* import java.util.*
@ -40,6 +42,7 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var tagDao: TagDao @Inject lateinit var tagDao: TagDao
@Inject lateinit var alarmDao: AlarmDao
@Test @Test
fun loadRemoteParentInfo() = runBlocking { fun loadRemoteParentInfo() = runBlocking {
@ -211,15 +214,17 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") .getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?.let { taskDao.fetch(it.task) } ?.let { taskDao.fetch(it.task) }
assertEquals(1612972355000, task!!.reminderSnooze) assertEquals(
listOf(Alarm(task!!.id, 1612972355000, TYPE_SNOOZE).apply { id = 1 }),
alarmDao.getAlarms(task.id)
)
} }
@Test @Test
fun pushSnoozeTime() = withTZ(CHICAGO) { fun pushSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList() val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask( val taskId = taskDao.createNew(newTask())
with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30)) alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE))
))
caldavDao.insert(newCaldavTask( caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid), with(CALENDAR, list.uuid),
@ -237,9 +242,8 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
@Test @Test
fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) { fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList() val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask( val taskId = taskDao.createNew(newTask())
with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30)) alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE))
))
caldavDao.insert(newCaldavTask( caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid), with(CALENDAR, list.uuid),
@ -261,8 +265,12 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
synchronizer.sync() synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?: throw IllegalStateException("Missing task")
taskDao.snooze(listOf(task!!.task), 0L) val snooze = alarmDao.getSnoozed(listOf(task.task))
assertEquals(1, snooze.size)
alarmDao.delete(snooze.first())
assertTrue(alarmDao.getSnoozed(listOf(task.task)).isEmpty())
taskDao.touch(task.task)
synchronizer.sync() synchronizer.sync()

@ -0,0 +1,32 @@
package org.tasks.makers
import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.Property.newProperty
import com.natpryce.makeiteasy.PropertyLookup
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.jobs.AlarmEntry
import org.tasks.makers.Maker.make
import org.tasks.time.DateTime
object AlarmEntryMaker {
val ID: Property<AlarmEntry, Long> = newProperty()
val TASK: Property<AlarmEntry, Long> = newProperty()
val TIME: Property<AlarmEntry, DateTime> = newProperty()
val TYPE: Property<AlarmEntry, Int> = newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<AlarmEntry> ->
AlarmEntry(
lookup.valueOf(ID, 0L),
lookup.valueOf(TASK, 0L),
lookup.valueOf(TIME, newDateTime()).millis,
lookup.valueOf(TYPE, TYPE_DATE_TIME)
)
}
fun newAlarmEntry(vararg properties: PropertyValue<in AlarmEntry?, *>): AlarmEntry {
return make(instantiator, *properties)
}
}

@ -17,14 +17,12 @@ object TaskMaker {
val DUE_DATE: Property<Task, DateTime?> = newProperty() val DUE_DATE: Property<Task, DateTime?> = newProperty()
val DUE_TIME: Property<Task, DateTime?> = newProperty() val DUE_TIME: Property<Task, DateTime?> = newProperty()
val REMINDER_LAST: Property<Task, DateTime?> = newProperty() val REMINDER_LAST: Property<Task, DateTime?> = newProperty()
val RANDOM_REMINDER_PERIOD: Property<Task, Long> = newProperty()
val HIDE_TYPE: Property<Task, Int> = newProperty() val HIDE_TYPE: Property<Task, Int> = newProperty()
val REMINDERS: Property<Task, Int> = newProperty() val REMINDERS: Property<Task, Int> = newProperty()
val MODIFICATION_TIME: Property<Task, DateTime> = newProperty() val MODIFICATION_TIME: Property<Task, DateTime> = newProperty()
val CREATION_TIME: Property<Task, DateTime> = newProperty() val CREATION_TIME: Property<Task, DateTime> = newProperty()
val COMPLETION_TIME: Property<Task, DateTime> = newProperty() val COMPLETION_TIME: Property<Task, DateTime> = newProperty()
val DELETION_TIME: Property<Task, DateTime?> = newProperty() val DELETION_TIME: Property<Task, DateTime?> = newProperty()
val SNOOZE_TIME: Property<Task, DateTime?> = newProperty()
val RECUR: Property<Task, String?> = newProperty() val RECUR: Property<Task, String?> = newProperty()
val AFTER_COMPLETE: Property<Task, Boolean> = newProperty() val AFTER_COMPLETE: Property<Task, Boolean> = newProperty()
val TITLE: Property<Task, String?> = newProperty() val TITLE: Property<Task, String?> = newProperty()
@ -63,10 +61,6 @@ object TaskMaker {
if (deletedTime != null) { if (deletedTime != null) {
task.deletionDate = deletedTime.millis task.deletionDate = deletedTime.millis
} }
val snoozeTime = lookup.valueOf(SNOOZE_TIME, null as DateTime?)
if (snoozeTime != null) {
task.reminderSnooze = snoozeTime.millis
}
val hideType = lookup.valueOf(HIDE_TYPE, -1) val hideType = lookup.valueOf(HIDE_TYPE, -1)
if (hideType >= 0) { if (hideType >= 0) {
task.hideUntil = task.createHideUntil(hideType, 0) task.hideUntil = task.createHideUntil(hideType, 0)
@ -79,10 +73,6 @@ object TaskMaker {
if (reminderLast != null) { if (reminderLast != null) {
task.reminderLast = reminderLast.millis task.reminderLast = reminderLast.millis
} }
val randomReminderPeriod = lookup.valueOf(RANDOM_REMINDER_PERIOD, 0L)
if (randomReminderPeriod > 0) {
task.reminderPeriod = randomReminderPeriod
}
lookup.valueOf(RECUR, null as String?)?.let { lookup.valueOf(RECUR, null as String?)?.let {
task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false)) task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false))
} }

@ -0,0 +1,91 @@
package com.todoroo.astrid.alarms
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import org.tasks.data.Alarm
import org.tasks.jobs.AlarmEntry
import org.tasks.preferences.Preferences
import org.tasks.reminders.Random
import org.tasks.time.DateTimeUtils.withMillisOfDay
import javax.inject.Inject
class AlarmCalculator(
private val random: Random,
private val defaultTimeProvider: () -> Int,
){
@Inject
internal constructor(
preferences: Preferences
) : this(Random(), { preferences.defaultDueTime })
fun toAlarmEntry(task: Task, alarm: Alarm): AlarmEntry? {
val trigger = when (alarm.type) {
Alarm.TYPE_SNOOZE,
Alarm.TYPE_DATE_TIME ->
alarm.time
Alarm.TYPE_REL_START ->
when {
task.hasStartTime() ->
task.hideUntil + alarm.time
task.hasStartDate() ->
task.hideUntil.withMillisOfDay(defaultTimeProvider()) + alarm.time
else ->
AlarmService.NO_ALARM
}
Alarm.TYPE_REL_END ->
when {
task.hasDueTime() ->
task.dueDate + alarm.time
task.hasDueDate() ->
task.dueDate.withMillisOfDay(defaultTimeProvider()) + alarm.time
else ->
AlarmService.NO_ALARM
}
Alarm.TYPE_RANDOM ->
calculateNextRandomReminder(random, task, alarm.time)
else ->
AlarmService.NO_ALARM
}
return when {
trigger <= AlarmService.NO_ALARM ->
null
trigger > task.reminderLast || alarm.type == Alarm.TYPE_SNOOZE ->
AlarmEntry(alarm.id, alarm.task, trigger, alarm.type)
alarm.repeat > 0 -> {
val past = (task.reminderLast - trigger) / alarm.interval
val next = trigger + (past + 1) * alarm.interval
if (past < alarm.repeat && next > task.reminderLast) {
AlarmEntry(alarm.id, alarm.task, next, alarm.type)
} else {
null
}
}
else ->
null
}
}
/** Schedules alarms for a single task */
/**
* Calculate the next alarm time for random reminders.
*
*
* We take the last reminder time and add approximately the reminder period. If it's still in
* the past, we set it to some time in the near future.
*/
private fun calculateNextRandomReminder(random: Random, task: Task, reminderPeriod: Long): Long {
if (reminderPeriod > 0) {
var `when` = task.reminderLast
if (`when` == 0L) {
`when` = task.creationDate
}
`when` += (reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong()
if (`when` < DateUtilities.now()) {
`when` =
DateUtilities.now() + ((0.5f + 6 * random.nextFloat()) * DateUtilities.ONE_HOUR).toLong()
}
return `when`
}
return AlarmService.NO_ALARM
}
}

@ -8,15 +8,11 @@ package com.todoroo.astrid.alarms
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.AlarmDao import org.tasks.data.AlarmDao
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue import org.tasks.jobs.NotificationQueue
import org.tasks.preferences.Preferences import org.tasks.notifications.NotificationManager
import org.tasks.time.DateTimeUtils.withMillisOfDay
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,10 +26,10 @@ class AlarmService @Inject constructor(
private val alarmDao: AlarmDao, private val alarmDao: AlarmDao,
private val jobs: NotificationQueue, private val jobs: NotificationQueue,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val preferences: Preferences,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val notificationManager: NotificationManager,
private val alarmCalculator: AlarmCalculator,
) { ) {
suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId) suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
/** /**
@ -50,8 +46,7 @@ class AlarmService @Inject constructor(
it.time == existing.time && it.time == existing.time &&
it.repeat == existing.repeat && it.repeat == existing.repeat &&
it.interval == existing.interval it.interval == existing.interval
}) { }) {
jobs.cancelAlarm(existing.id)
alarmDao.delete(existing) alarmDao.delete(existing)
changed = true changed = true
} }
@ -68,75 +63,48 @@ class AlarmService @Inject constructor(
return changed return changed
} }
private suspend fun getActiveAlarmsForTask(taskId: Long): List<Alarm> =
alarmDao.getActiveAlarms(taskId)
suspend fun scheduleAllAlarms() { suspend fun scheduleAllAlarms() {
alarmDao alarmDao
.getActiveAlarms() .getActiveAlarms()
.groupBy { it.task } .groupBy { it.task }
.forEach { (taskId, alarms) -> .forEach { (taskId, alarms) ->
val task = taskDao.fetch(taskId) ?: return@forEach val task = taskDao.fetch(taskId) ?: return@forEach
alarms.forEach { scheduleAlarm(task, it) } scheduleAlarms(task, alarms)
} }
} }
suspend fun cancelAlarms(taskId: Long) { fun cancelAlarms(taskId: Long) {
for (alarm in getActiveAlarmsForTask(taskId)) { jobs.cancelForTask(taskId)
jobs.cancelAlarm(alarm.id) }
}
suspend fun snooze(time: Long, taskIds: List<Long>) {
notificationManager.cancel(taskIds)
alarmDao.getSnoozed(taskIds).let { alarmDao.delete(it) }
taskIds.map { Alarm(it, time, TYPE_SNOOZE) }.let { alarmDao.insert(it) }
taskDao.touch(taskIds)
scheduleAlarms(taskIds)
}
suspend fun scheduleAlarms(taskIds: List<Long>) {
taskDao.fetch(taskIds).forEach { scheduleAlarms(it) }
} }
/** Schedules alarms for a single task */ /** Schedules alarms for a single task */
suspend fun scheduleAlarms(task: Task) { suspend fun scheduleAlarms(task: Task) {
getActiveAlarmsForTask(task.id).forEach { scheduleAlarm(task, it) } scheduleAlarms(task, alarmDao.getActiveAlarms(task.id))
} }
/** Schedules alarms for a single task */ private fun scheduleAlarms(task: Task, alarms: List<Alarm>) {
private fun scheduleAlarm(task: Task, alarm: Alarm?) { jobs.cancelForTask(task.id)
if (alarm == null) { val alarmEntries = alarms.mapNotNull {
return alarmCalculator.toAlarmEntry(task, it)
}
val trigger = when (alarm.type) {
TYPE_DATE_TIME ->
alarm.time
TYPE_REL_START ->
when {
task.hasStartTime() ->
task.hideUntil + alarm.time
task.hasStartDate() ->
task.hideUntil.withMillisOfDay(preferences.defaultDueTime) + alarm.time
else ->
NO_ALARM
}
TYPE_REL_END ->
when {
task.hasDueTime() ->
task.dueDate + alarm.time
task.hasDueDate() ->
task.dueDate.withMillisOfDay(preferences.defaultDueTime) + alarm.time
else ->
NO_ALARM
}
else -> NO_ALARM
}
jobs.cancelAlarm(alarm.id)
when {
trigger <= NO_ALARM ->
{}
trigger > task.reminderLast ->
jobs.add(AlarmEntry(alarm.id, alarm.task, trigger))
alarm.repeat > 0 -> {
val past = (task.reminderLast - trigger) / alarm.interval
val next = trigger + (past + 1) * alarm.interval
if (past < alarm.repeat && next > task.reminderLast) {
jobs.add(AlarmEntry(alarm.id, alarm.task, next))
}
}
} }
val next =
alarmEntries.find { it.type == TYPE_SNOOZE } ?: alarmEntries.minByOrNull { it.time }
next?.let { jobs.add(it) }
} }
companion object { companion object {
private const val NO_ALARM = 0L internal const val NO_ALARM = 0L
} }
} }

@ -5,11 +5,9 @@
*/ */
package com.todoroo.astrid.dao package com.todoroo.astrid.dao
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.reminders.ReminderService
import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.timers.TimerPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -29,7 +27,6 @@ import javax.inject.Inject
class TaskDao @Inject constructor( class TaskDao @Inject constructor(
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val reminderService: ReminderService,
private val refreshScheduler: RefreshScheduler, private val refreshScheduler: RefreshScheduler,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val notificationManager: NotificationManager, private val notificationManager: NotificationManager,
@ -54,11 +51,6 @@ class TaskDao @Inject constructor(
suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long) = suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long) =
taskDao.setCompletionDate(remoteIds, completionDate) taskDao.setCompletionDate(remoteIds, completionDate)
suspend fun snooze(taskIds: List<Long>, snoozeTime: Long, updateTime: Long = now()) {
taskDao.snooze(taskIds, snoozeTime, updateTime)
syncAdapters.sync()
}
suspend fun getGoogleTasksToPush(account: String): List<Task> = suspend fun getGoogleTasksToPush(account: String): List<Task> =
taskDao.getGoogleTasksToPush(account) taskDao.getGoogleTasksToPush(account)
@ -128,16 +120,12 @@ class TaskDao @Inject constructor(
timerPlugin.stopTimer(task) timerPlugin.stopTimer(task)
} }
} }
if (task.reminderSnooze.isAfterNow()) {
notificationManager.cancel(task.id)
}
if (task.dueDate != original?.dueDate && task.dueDate.isAfterNow()) { if (task.dueDate != original?.dueDate && task.dueDate.isAfterNow()) {
notificationManager.cancel(task.id) notificationManager.cancel(task.id)
} }
if (completionDateModified || deletionDateModified) { if (completionDateModified || deletionDateModified) {
geofenceApi.update(task.id) geofenceApi.update(task.id)
} }
reminderService.scheduleAlarm(task)
alarmService.scheduleAlarms(task) alarmService.scheduleAlarms(task)
refreshScheduler.scheduleRefresh(task) refreshScheduler.scheduleRefresh(task)
if (!task.isSuppressRefresh()) { if (!task.isSuppressRefresh()) {

@ -84,7 +84,7 @@ class Task : Parcelable {
@SerializedName("ringFlags", alternate = ["reminderFlags"]) @SerializedName("ringFlags", alternate = ["reminderFlags"])
var ringFlags = 0 var ringFlags = 0
/** Reminder period, in milliseconds. 0 means disabled */ @Deprecated("old random reminders")
@ColumnInfo(name = "notifications") @ColumnInfo(name = "notifications")
var reminderPeriod = 0L var reminderPeriod = 0L
@ -92,7 +92,7 @@ class Task : Parcelable {
@ColumnInfo(name = "lastNotified") @ColumnInfo(name = "lastNotified")
var reminderLast = 0L var reminderLast = 0L
/** Unixtime snooze is set (0 -> no snooze) */ @Deprecated("old snooze reminders")
@ColumnInfo(name = "snoozeTime") @ColumnInfo(name = "snoozeTime")
var reminderSnooze = 0L var reminderSnooze = 0L
@ -143,8 +143,6 @@ class Task : Parcelable {
recurrence = parcel.readString() recurrence = parcel.readString()
ringFlags = parcel.readInt() ringFlags = parcel.readInt()
reminderLast = parcel.readLong() reminderLast = parcel.readLong()
reminderPeriod = parcel.readLong()
reminderSnooze = parcel.readLong()
repeatUntil = parcel.readLong() repeatUntil = parcel.readLong()
timerStart = parcel.readLong() timerStart = parcel.readLong()
title = parcel.readString() title = parcel.readString()
@ -293,8 +291,6 @@ class Task : Parcelable {
dest.writeString(recurrence) dest.writeString(recurrence)
dest.writeInt(ringFlags) dest.writeInt(ringFlags)
dest.writeLong(reminderLast) dest.writeLong(reminderLast)
dest.writeLong(reminderPeriod)
dest.writeLong(reminderSnooze)
dest.writeLong(repeatUntil) dest.writeLong(repeatUntil)
dest.writeLong(timerStart) dest.writeLong(timerStart)
dest.writeString(title) dest.writeString(title)
@ -323,13 +319,11 @@ class Task : Parcelable {
&& estimatedSeconds == task.estimatedSeconds && estimatedSeconds == task.estimatedSeconds
&& elapsedSeconds == task.elapsedSeconds && elapsedSeconds == task.elapsedSeconds
&& ringFlags == task.ringFlags && ringFlags == task.ringFlags
&& reminderPeriod == task.reminderPeriod
&& recurrence == task.recurrence && recurrence == task.recurrence
&& repeatUntil == task.repeatUntil && repeatUntil == task.repeatUntil
&& calendarURI == task.calendarURI && calendarURI == task.calendarURI
&& parent == task.parent && parent == task.parent
&& remoteId == task.remoteId && remoteId == task.remoteId
&& reminderSnooze == task.reminderSnooze
} }
fun googleTaskUpToDate(original: Task?): Boolean { fun googleTaskUpToDate(original: Task?): Boolean {
@ -363,7 +357,6 @@ class Task : Parcelable {
&& parent == original.parent && parent == original.parent
&& repeatUntil == original.repeatUntil && repeatUntil == original.repeatUntil
&& isCollapsed == original.isCollapsed && isCollapsed == original.isCollapsed
&& reminderSnooze == original.reminderSnooze
} }
val isSaved: Boolean val isSaved: Boolean
@ -385,6 +378,10 @@ class Task : Parcelable {
putTransitory(TRANS_REMINDERS, flags) putTransitory(TRANS_REMINDERS, flags)
} }
var randomReminder: Long
get() = getTransitory(TRANS_RANDOM) ?: 0L
set(value) = putTransitory(TRANS_RANDOM, value)
@Synchronized @Synchronized
fun putTransitory(key: String, value: Any) { fun putTransitory(key: String, value: Any) {
if (transitoryData == null) { if (transitoryData == null) {
@ -439,9 +436,7 @@ class Task : Parcelable {
if (elapsedSeconds != other.elapsedSeconds) return false if (elapsedSeconds != other.elapsedSeconds) return false
if (timerStart != other.timerStart) return false if (timerStart != other.timerStart) return false
if (ringFlags != other.ringFlags) return false if (ringFlags != other.ringFlags) return false
if (reminderPeriod != other.reminderPeriod) return false
if (reminderLast != other.reminderLast) return false if (reminderLast != other.reminderLast) return false
if (reminderSnooze != other.reminderSnooze) return false
if (recurrence != other.recurrence) return false if (recurrence != other.recurrence) return false
if (repeatUntil != other.repeatUntil) return false if (repeatUntil != other.repeatUntil) return false
if (calendarURI != other.calendarURI) return false if (calendarURI != other.calendarURI) return false
@ -468,9 +463,7 @@ class Task : Parcelable {
result = 31 * result + elapsedSeconds result = 31 * result + elapsedSeconds
result = 31 * result + timerStart.hashCode() result = 31 * result + timerStart.hashCode()
result = 31 * result + ringFlags result = 31 * result + ringFlags
result = 31 * result + reminderPeriod.hashCode()
result = 31 * result + reminderLast.hashCode() result = 31 * result + reminderLast.hashCode()
result = 31 * result + reminderSnooze.hashCode()
result = 31 * result + (recurrence?.hashCode() ?: 0) result = 31 * result + (recurrence?.hashCode() ?: 0)
result = 31 * result + repeatUntil.hashCode() result = 31 * result + repeatUntil.hashCode()
result = 31 * result + (calendarURI?.hashCode() ?: 0) result = 31 * result + (calendarURI?.hashCode() ?: 0)
@ -482,7 +475,7 @@ class Task : Parcelable {
} }
override fun toString(): String { override fun toString(): String {
return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderPeriod=$reminderPeriod, reminderLast=$reminderLast, reminderSnooze=$reminderSnooze, recurrence=$recurrence, repeatUntil=$repeatUntil, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData)" return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderLast=$reminderLast, recurrence=$recurrence, repeatUntil=$repeatUntil, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData)"
} }
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@ -572,6 +565,7 @@ class Task : Parcelable {
private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh" private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh"
const val TRANS_REMINDERS = "reminders" const val TRANS_REMINDERS = "reminders"
const val TRANS_RANDOM = "random"
private val INVALID_COUNT = ";?COUNT=-1".toRegex() private val INVALID_COUNT = ";?COUNT=-1".toRegex()

@ -1,114 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.reminders
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import org.tasks.data.TaskDao
import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry
import org.tasks.reminders.Random
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ReminderService internal constructor(
private val jobs: NotificationQueue,
private val random: Random,
private val taskDao: TaskDao,
) {
@Inject
internal constructor(
notificationQueue: NotificationQueue,
taskDao: TaskDao
) : this(notificationQueue, Random(), taskDao)
suspend fun scheduleAlarm(id: Long) = scheduleAllAlarms(listOf(id))
suspend fun scheduleAllAlarms(taskIds: List<Long>) = scheduleAlarms(taskDao.fetch(taskIds))
suspend fun scheduleAllAlarms() = scheduleAlarms(taskDao.getTasksWithReminders())
fun scheduleAlarm(task: Task) = scheduleAlarms(listOf(task))
private fun scheduleAlarms(tasks: List<Task>) =
tasks
.mapNotNull { getReminderEntry(it) }
.let { jobs.add(it) }
fun cancelReminder(taskId: Long) {
jobs.cancelReminder(taskId)
}
private fun getReminderEntry(task: Task?): ReminderEntry? {
if (task == null || !task.isSaved) {
return null
}
val taskId = task.id
// Make sure no alarms are scheduled other than the next one. When that one is shown, it
// will schedule the next one after it, and so on and so forth.
cancelReminder(taskId)
if (task.isCompleted || task.isDeleted) {
return null
}
// snooze reminder
val whenSnooze = calculateNextSnoozeReminder(task)
// random reminders
val whenRandom = calculateNextRandomReminder(task)
// snooze trumps all
return when {
whenSnooze != NO_ALARM -> ReminderEntry(taskId, whenSnooze, TYPE_SNOOZE)
whenRandom != NO_ALARM -> ReminderEntry(taskId, whenRandom, TYPE_RANDOM)
else -> null
}
}
private fun calculateNextSnoozeReminder(task: Task): Long {
return if (task.reminderSnooze > task.reminderLast) {
task.reminderSnooze
} else NO_ALARM
}
/**
* Calculate the next alarm time for random reminders.
*
*
* We take the last reminder time and add approximately the reminder period. If it's still in
* the past, we set it to some time in the near future.
*/
private fun calculateNextRandomReminder(task: Task): Long {
val reminderPeriod = task.reminderPeriod
if (reminderPeriod > 0) {
var `when` = task.reminderLast
if (`when` == 0L) {
`when` = task.creationDate
}
`when` += (reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong()
if (`when` < DateUtilities.now()) {
`when` = DateUtilities.now() + ((0.5f + 6 * random.nextFloat()) * DateUtilities.ONE_HOUR).toLong()
}
return `when`
}
return NO_ALARM
}
companion object {
const val TYPE_DUE = 0
const val TYPE_OVERDUE = 1
const val TYPE_RANDOM = 2
const val TYPE_SNOOZE = 3
const val TYPE_ALARM = 4
const val TYPE_GEOFENCE_ENTER = 5
const val TYPE_GEOFENCE_EXIT = 6
const val TYPE_START = 7
private const val NO_ALARM = Long.MAX_VALUE
}
}

@ -17,6 +17,7 @@ import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay import net.fortuna.ical4j.model.WeekDay
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime import org.tasks.time.DateTime
@ -67,7 +68,6 @@ class RepeatTaskHelper @Inject constructor(
task.setRecurrence(rrule.toString(), repeatAfterCompletion) task.setRecurrence(rrule.toString(), repeatAfterCompletion)
} }
task.reminderLast = 0L task.reminderLast = 0L
task.reminderSnooze = 0L
task.completionDate = 0L task.completionDate = 0L
task.setDueDateAdjustingHideUntil(newDueDate) task.setDueDateAdjustingHideUntil(newDueDate)
gcalHelper.rescheduleRepeatingTask(task) gcalHelper.rescheduleRepeatingTask(task)
@ -116,13 +116,13 @@ class RepeatTaskHelper @Inject constructor(
return return
} }
alarmService.getAlarms(taskId) alarmService.getAlarms(taskId)
.takeIf { it.isNotEmpty() } .filter { it.type != TYPE_SNOOZE }
?.onEach { .onEach {
if (it.type == Alarm.TYPE_DATE_TIME) { if (it.type == Alarm.TYPE_DATE_TIME) {
it.time += newDueDate - oldDueDate it.time += newDueDate - oldDueDate
} }
} }
?.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) } .let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
} }
companion object { companion object {

@ -18,6 +18,7 @@ import com.todoroo.astrid.utility.TitleParser.parse
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.whenDue import org.tasks.data.Alarm.Companion.whenDue
import org.tasks.data.Alarm.Companion.whenOverdue import org.tasks.data.Alarm.Companion.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted import org.tasks.data.Alarm.Companion.whenStarted
@ -176,8 +177,10 @@ class TaskCreator @Inject constructor(
companion object { companion object {
private fun setDefaultReminders(preferences: Preferences, task: Task) { private fun setDefaultReminders(preferences: Preferences, task: Task) {
task.reminderPeriod = (DateUtilities.ONE_HOUR task.randomReminder = DateUtilities.ONE_HOUR * preferences.getIntegerFromString(
* preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, 0)) R.string.p_rmd_default_random_hours,
0
)
task.defaultReminders(preferences.defaultReminders) task.defaultReminders(preferences.defaultReminders)
task.ringFlags = preferences.defaultRingMode task.ringFlags = preferences.defaultRingMode
} }
@ -197,6 +200,9 @@ class TaskCreator @Inject constructor(
add(whenOverdue(id)) add(whenOverdue(id))
} }
} }
if (randomReminder > 0) {
add(Alarm(id, randomReminder, TYPE_RANDOM))
}
} }
} }
} }

@ -5,10 +5,19 @@ import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.gcal.GCalHelper
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.* import org.tasks.data.Alarm
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.Geofence
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.LocationDao
import org.tasks.data.Tag
import org.tasks.data.TagDao
import org.tasks.data.TagDataDao
import org.tasks.db.DbUtils.dbchunk import org.tasks.db.DbUtils.dbchunk
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class TaskDuplicator @Inject constructor( class TaskDuplicator @Inject constructor(
@ -70,7 +79,7 @@ class TaskDuplicator @Inject constructor(
} }
val alarms = alarmDao.getAlarms(originalId) val alarms = alarmDao.getAlarms(originalId)
if (alarms.isNotEmpty()) { if (alarms.isNotEmpty()) {
alarmDao.insert(alarms.map { Alarm(clone.id, it.time) }) alarmDao.insert(alarms.map { Alarm(clone.id, it.time, it.type) })
} }
gcalHelper.createTaskEventIfEnabled(clone) gcalHelper.createTaskEventIfEnabled(clone)
taskDao.save(clone, null) // TODO: delete me taskDao.save(clone, null) // TODO: delete me

@ -13,6 +13,8 @@ import android.widget.ArrayAdapter
import android.widget.Spinner import android.widget.Spinner
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R import org.tasks.R
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.ui.TaskEditViewModel import org.tasks.ui.TaskEditViewModel
/** /**
@ -39,7 +41,10 @@ internal class RandomReminderControlSet(context: Context, parentView: View, remi
periodSpinner.adapter = adapter periodSpinner.adapter = adapter
periodSpinner.onItemSelectedListener = object : OnItemSelectedListener { periodSpinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
vm.reminderPeriod = hours[position] * DateUtilities.ONE_HOUR vm.selectedAlarms?.removeIf { it.type == TYPE_RANDOM }
vm.selectedAlarms?.add(
Alarm(vm.task?.id ?: 0, hours[position] * DateUtilities.ONE_HOUR, TYPE_RANDOM)
)
} }
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}

@ -58,9 +58,6 @@ class ReminderControlSet : TaskEditControlFragment() {
viewModel.ringFiveTimes!! -> setRingMode(1) viewModel.ringFiveTimes!! -> setRingMode(1)
else -> setRingMode(0) else -> setRingMode(0)
} }
if (viewModel.reminderPeriod!! > 0) {
addRandomReminder(viewModel.reminderPeriod!!)
}
viewModel.selectedAlarms?.forEach(this::addAlarmRow) viewModel.selectedAlarms?.forEach(this::addAlarmRow)
} }
@ -96,15 +93,16 @@ class ReminderControlSet : TaskEditControlFragment() {
} }
private fun addAlarm(selected: String) { private fun addAlarm(selected: String) {
val id = viewModel.task?.id ?: 0
when (selected) { when (selected) {
getString(R.string.when_started) -> getString(R.string.when_started) ->
addAlarmRow(whenStarted(viewModel.task?.id ?: 0)) addAlarmRow(whenStarted(id))
getString(R.string.when_due) -> getString(R.string.when_due) ->
addAlarmRow(whenDue(viewModel.task?.id ?: 0)) addAlarmRow(whenDue(id))
getString(R.string.when_overdue) -> getString(R.string.when_overdue) ->
addAlarmRow(whenOverdue(viewModel.task?.id ?: 0)) addAlarmRow(whenOverdue(id))
getString(R.string.randomly) -> getString(R.string.randomly) ->
addRandomReminder(TimeUnit.DAYS.toMillis(14)) addAlarmRow(Alarm(id, TimeUnit.DAYS.toMillis(14), TYPE_RANDOM))
getString(R.string.pick_a_date_and_time) -> getString(R.string.pick_a_date_and_time) ->
addNewAlarm() addNewAlarm()
} }
@ -144,7 +142,7 @@ class ReminderControlSet : TaskEditControlFragment() {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L) val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L)
if (viewModel.selectedAlarms?.any { it.type == TYPE_DATE_TIME && timestamp == it.time } == false) { if (viewModel.selectedAlarms?.any { it.type == TYPE_DATE_TIME && timestamp == it.time } == false) {
val alarm = Alarm(viewModel.task?.id ?: 0, timestamp) val alarm = Alarm(viewModel.task?.id ?: 0, timestamp, TYPE_DATE_TIME)
viewModel.selectedAlarms?.add(alarm) viewModel.selectedAlarms?.add(alarm)
addAlarmRow(alarm) addAlarmRow(alarm)
} }
@ -155,14 +153,22 @@ class ReminderControlSet : TaskEditControlFragment() {
} }
private fun addAlarmRow(alarm: Alarm) { private fun addAlarmRow(alarm: Alarm) {
addAlarmRow(alarm) { val alarmRow = addAlarmRow(alarm) {
viewModel.selectedAlarms?.removeIf { if (alarm.type == TYPE_RANDOM) {
it.type == alarm.type && viewModel.selectedAlarms?.removeIf { it.type == TYPE_RANDOM }
it.time == alarm.time && randomControlSet = null
it.repeat == alarm.repeat && } else {
it.interval == alarm.interval viewModel.selectedAlarms?.removeIf {
it.type == alarm.type &&
it.time == alarm.time &&
it.repeat == alarm.repeat &&
it.interval == alarm.interval
}
} }
} }
if (alarm.type == TYPE_RANDOM) {
randomControlSet = RandomReminderControlSet(activity, alarmRow, alarm.time, viewModel)
}
} }
private fun addNewAlarm() { private fun addNewAlarm() {
@ -210,14 +216,6 @@ class ReminderControlSet : TaskEditControlFragment() {
return options return options
} }
private fun addRandomReminder(reminderPeriod: Long) {
val alarmRow = addAlarmRow(Alarm(viewModel.task?.id ?: 0, 0, TYPE_RANDOM)) {
viewModel.reminderPeriod = 0
randomControlSet = null
}
randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod, viewModel)
}
companion object { companion object {
const val TAG = R.string.TEA_ctrl_reminders_pref const val TAG = R.string.TEA_ctrl_reminders_pref
private const val REQUEST_NEW_ALARM = 12152 private const val REQUEST_NEW_ALARM = 12152

@ -4,12 +4,12 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.reminders.ReminderService
import com.todoroo.astrid.reminders.ReminderService.Companion.TYPE_GEOFENCE_ENTER
import com.todoroo.astrid.reminders.ReminderService.Companion.TYPE_GEOFENCE_EXIT
import com.todoroo.astrid.voice.VoiceOutputAssistant import com.todoroo.astrid.voice.VoiceOutputAssistant
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_GEO_ENTER
import org.tasks.data.Alarm.Companion.TYPE_GEO_EXIT
import org.tasks.data.Geofence import org.tasks.data.Geofence
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.intents.TaskIntents import org.tasks.intents.TaskIntents
@ -78,7 +78,7 @@ class Notifier @Inject constructor(
.map { .map {
Notification().apply { Notification().apply {
taskId = it.task taskId = it.task
type = if (arrival) TYPE_GEOFENCE_ENTER else TYPE_GEOFENCE_EXIT type = if (arrival) TYPE_GEO_ENTER else TYPE_GEO_EXIT
timestamp = DateTimeUtils.currentTimeMillis() timestamp = DateTimeUtils.currentTimeMillis()
location = place location = place
} }
@ -92,7 +92,7 @@ class Notifier @Inject constructor(
.filter { .filter {
taskDao.fetch(it.taskId) taskDao.fetch(it.taskId)
?.let { task -> ?.let { task ->
if (it.type != ReminderService.TYPE_RANDOM) { if (it.type != Alarm.TYPE_RANDOM) {
ringFiveTimes = ringFiveTimes or task.isNotifyModeFive ringFiveTimes = ringFiveTimes or task.isNotifyModeFive
ringNonstop = ringNonstop or task.isNotifyModeNonstop ringNonstop = ringNonstop or task.isNotifyModeNonstop
} }

@ -40,7 +40,6 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.caldav.property.Invite import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCAccess import org.tasks.caldav.property.OCAccess
import org.tasks.caldav.property.OCInvite import org.tasks.caldav.property.OCInvite
@ -274,11 +273,8 @@ class CaldavSynchronizer @Inject constructor(
Timber.e("Invalid VCALENDAR: %s", fileName) Timber.e("Invalid VCALENDAR: %s", fileName)
return return
} }
var caldavTask = caldavDao.getTask(caldavCalendar.uuid!!, fileName) val caldavTask = caldavDao.getTask(caldavCalendar.uuid!!, fileName)
iCal.fromVtodo(caldavCalendar, caldavTask, remote, vtodo, fileName, eTag) iCal.fromVtodo(caldavCalendar, caldavTask, remote, vtodo, fileName, eTag)
caldavTask = caldavTask ?: caldavDao.getTask(caldavCalendar.uuid!!, fileName)
?: continue
alarmService.synchronizeAlarms(caldavTask.task, remote.reminders.toMutableSet())
} }
} }
caldavDao caldavDao

@ -3,8 +3,8 @@ package org.tasks.caldav
import at.bitfire.ical4android.DateUtils.ical4jTimeZone import at.bitfire.ical4android.DateUtils.ical4jTimeZone
import at.bitfire.ical4android.Task import at.bitfire.ical4android.Task
import at.bitfire.ical4android.Task.Companion.tasksFromReader import at.bitfire.ical4android.Task.Companion.tasksFromReader
import at.bitfire.ical4android.util.TimeApiExtensions.toDuration
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY_TIME import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY_TIME
@ -14,23 +14,18 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.ParameterList
import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.parameter.Related
import net.fortuna.ical4j.model.parameter.Related.* import net.fortuna.ical4j.model.parameter.Related.*
import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.Completed import net.fortuna.ical4j.model.property.Completed
import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.Description
import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.model.property.Due
import net.fortuna.ical4j.model.property.Geo import net.fortuna.ical4j.model.property.Geo
import net.fortuna.ical4j.model.property.RelatedTo import net.fortuna.ical4j.model.property.RelatedTo
import net.fortuna.ical4j.model.property.Repeat
import net.fortuna.ical4j.model.property.Status import net.fortuna.ical4j.model.property.Status
import net.fortuna.ical4j.model.property.Trigger
import net.fortuna.ical4j.model.property.XProperty import net.fortuna.ical4j.model.property.XProperty
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.GeoUtils.equalish import org.tasks.caldav.GeoUtils.equalish
@ -39,9 +34,8 @@ import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.caldav.extensions.toAlarms import org.tasks.caldav.extensions.toAlarms
import org.tasks.caldav.extensions.toVAlarms import org.tasks.caldav.extensions.toVAlarms
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_REL_END import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.AlarmDao import org.tasks.data.AlarmDao
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
@ -67,9 +61,6 @@ import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.StringReader import java.io.StringReader
import java.text.ParseException import java.text.ParseException
import java.time.Duration
import java.time.Instant
import java.time.temporal.TemporalAmount
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
@ -87,6 +78,7 @@ class iCalendar @Inject constructor(
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val alarmDao: AlarmDao, private val alarmDao: AlarmDao,
private val alarmService: AlarmService,
) { ) {
suspend fun setPlace(taskId: Long, geo: Geo?) { suspend fun setPlace(taskId: Long, geo: Geo?) {
@ -172,7 +164,9 @@ class iCalendar @Inject constructor(
remoteModel.geoPosition = localGeo remoteModel.geoPosition = localGeo
} }
remoteModel.alarms.removeAll(remoteModel.alarms.filtered) remoteModel.alarms.removeAll(remoteModel.alarms.filtered)
remoteModel.alarms.addAll(alarmDao.getAlarms(task.id).toVAlarms()) val alarms = alarmDao.getAlarms(task.id)
remoteModel.snooze = alarms.find { it.type == TYPE_SNOOZE }?.time
remoteModel.alarms.addAll(alarms.toVAlarms())
} }
suspend fun fromVtodo( suspend fun fromVtodo(
@ -191,6 +185,11 @@ class iCalendar @Inject constructor(
task.applyRemote(remote) task.applyRemote(remote)
setPlace(task.id, remote.geoPosition) setPlace(task.id, remote.geoPosition)
tagDao.applyTags(task, tagDataDao, getTags(remote.categories)) tagDao.applyTags(task, tagDataDao, getTags(remote.categories))
val randomReminders = alarmDao.getAlarms(task.id).filter { it.type == TYPE_RANDOM }
alarmService.synchronizeAlarms(
caldavTask.task,
remote.reminders.plus(randomReminders).toMutableSet()
)
task.suppressSync() task.suppressSync()
task.suppressRefresh() task.suppressRefresh()
taskDao.save(task) taskDao.save(task)
@ -362,7 +361,6 @@ class iCalendar @Inject constructor(
remote.due.apply(this) remote.due.apply(this)
remote.dtStart.apply(this) remote.dtStart.apply(this)
isCollapsed = remote.collapsed isCollapsed = remote.collapsed
reminderSnooze = remote.snooze ?: 0
} }
fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) { fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) {
@ -424,7 +422,6 @@ class iCalendar @Inject constructor(
parent = if (task.parent == 0L) null else caldavTask.remoteParent parent = if (task.parent == 0L) null else caldavTask.remoteParent
order = caldavTask.order order = caldavTask.order
collapsed = task.isCollapsed collapsed = task.isCollapsed
snooze = task.reminderSnooze
} }
val List<VAlarm>.filtered: List<VAlarm> val List<VAlarm>.filtered: List<VAlarm>
@ -433,7 +430,9 @@ class iCalendar @Inject constructor(
.filterNot { it.trigger.dateTime == IGNORE_ALARM } .filterNot { it.trigger.dateTime == IGNORE_ALARM }
val Task.reminders: List<Alarm> val Task.reminders: List<Alarm>
get() = alarms.filtered.toAlarms() get() = alarms.filtered.toAlarms().let { alarms ->
snooze?.let { time -> alarms.plus(Alarm(0, time, TYPE_SNOOZE))} ?: alarms
}
internal fun getDateTime(timestamp: Long): DateTime { internal fun getDateTime(timestamp: Long): DateTime {
val tz = ical4jTimeZone(TimeZone.getDefault().id) val tz = ical4jTimeZone(TimeZone.getDefault().id)

@ -46,7 +46,7 @@ class Alarm : Parcelable {
} }
@Ignore @Ignore
constructor(task: Long, time: Long, type: Int = 0, repeat: Int = 0, interval: Long = 0) { constructor(task: Long, time: Long, type: Int, repeat: Int = 0, interval: Long = 0) {
this.task = task this.task = task
this.time = time this.time = time
this.type = type this.type = type
@ -104,6 +104,9 @@ class Alarm : Parcelable {
const val TYPE_REL_START = 1 const val TYPE_REL_START = 1
const val TYPE_REL_END = 2 const val TYPE_REL_END = 2
const val TYPE_RANDOM = 3 const val TYPE_RANDOM = 3
const val TYPE_SNOOZE = 4
const val TYPE_GEO_ENTER = 5
const val TYPE_GEO_EXIT = 6
fun whenStarted(task: Long) = Alarm(task, 0, TYPE_REL_START) fun whenStarted(task: Long) = Alarm(task, 0, TYPE_REL_START)

@ -5,6 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
@Dao @Dao
interface AlarmDao { interface AlarmDao {
@ -27,12 +28,21 @@ WHERE tasks._id = :taskId
""") """)
suspend fun getActiveAlarms(taskId: Long): List<Alarm> suspend fun getActiveAlarms(taskId: Long): List<Alarm>
@Query("SELECT * FROM alarms WHERE type = $TYPE_SNOOZE AND task IN (:taskIds)")
suspend fun getSnoozed(taskIds: List<Long>): List<Alarm>
@Query("SELECT * FROM alarms WHERE task = :taskId") @Query("SELECT * FROM alarms WHERE task = :taskId")
suspend fun getAlarms(taskId: Long): List<Alarm> suspend fun getAlarms(taskId: Long): List<Alarm>
@Query("DELETE FROM alarms WHERE _id IN(:alarmIds)")
suspend fun deleteByIds(alarmIds: List<Long>)
@Delete @Delete
suspend fun delete(alarm: Alarm) suspend fun delete(alarm: Alarm)
@Delete
suspend fun delete(alarms: List<Alarm>)
@Insert @Insert
suspend fun insert(alarm: Alarm): Long suspend fun insert(alarm: Alarm): Long

@ -1,10 +1,16 @@
package org.tasks.data package org.tasks.data
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.FilterListItem.NO_ORDER import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.filters.LocationFilters import org.tasks.filters.LocationFilters
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.currentTimeMillis
@ -37,14 +43,16 @@ interface LocationDao {
@Query("SELECT geofences.* FROM geofences" @Query("SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task" + " INNER JOIN tasks ON tasks._id = geofences.task"
+ " LEFT JOIN alarms ON tasks._id = alarms.task AND alarms.type == $TYPE_SNOOZE"
+ " WHERE place = :place AND arrival = 1 AND tasks.completed = 0" + " WHERE place = :place AND arrival = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now") + " AND tasks.deleted = 0 AND (alarms._id IS NULL OR alarms.time < :now) AND tasks.hideUntil < :now")
suspend fun getArrivalGeofences(place: String, now: Long = now()): List<Geofence> suspend fun getArrivalGeofences(place: String, now: Long = now()): List<Geofence>
@Query("SELECT geofences.* FROM geofences" @Query("SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task" + " INNER JOIN tasks ON tasks._id = geofences.task"
+ " LEFT JOIN alarms ON tasks._id = alarms.task AND alarms.type == $TYPE_SNOOZE"
+ " WHERE place = :place AND departure = 1 AND tasks.completed = 0" + " WHERE place = :place AND departure = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now") + " AND tasks.deleted = 0 AND (alarms._id IS NULL OR alarms.time < :now) AND tasks.hideUntil < :now")
suspend fun getDepartureGeofences(place: String, now: Long = now()): List<Geofence> suspend fun getDepartureGeofences(place: String, now: Long = now()): List<Geofence>
@Query("SELECT * FROM geofences" @Query("SELECT * FROM geofences"

@ -58,9 +58,6 @@ abstract class TaskDao(private val database: Database) {
@Query("UPDATE tasks SET completed = :completionDate, modified = :updateTime WHERE remoteId IN (:remoteIds)") @Query("UPDATE tasks SET completed = :completionDate, modified = :updateTime WHERE remoteId IN (:remoteIds)")
abstract suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long, updateTime: Long = now()) abstract suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long, updateTime: Long = now())
@Query("UPDATE tasks SET snoozeTime = :snoozeTime, modified = :updateTime WHERE _id in (:taskIds)")
internal abstract suspend fun snooze(taskIds: List<Long>, snoozeTime: Long, updateTime: Long = now())
@Query("SELECT tasks.* FROM tasks " @Query("SELECT tasks.* FROM tasks "
+ "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task "
+ "WHERE gt_list_id IN (SELECT gtl_remote_id FROM google_task_lists WHERE gtl_account = :account)" + "WHERE gt_list_id IN (SELECT gtl_remote_id FROM google_task_lists WHERE gtl_account = :account)"
@ -78,10 +75,6 @@ abstract class TaskDao(private val database: Database) {
ORDER BY created""") ORDER BY created""")
abstract suspend fun getCaldavTasksToPush(calendar: String): List<Task> abstract suspend fun getCaldavTasksToPush(calendar: String): List<Task>
@Query("SELECT * FROM TASKS "
+ "WHERE completed = 0 AND deleted = 0 AND (notificationFlags > 0 OR notifications > 0)")
abstract suspend fun getTasksWithReminders(): List<Task>
// --- SQL clause generators // --- SQL clause generators
@Query("SELECT * FROM tasks") @Query("SELECT * FROM tasks")
abstract suspend fun getAll(): List<Task> abstract suspend fun getAll(): List<Task>
@ -155,7 +148,7 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas
internal abstract suspend fun setParentInternal(parent: Long, children: List<Long>) internal abstract suspend fun setParentInternal(parent: Long, children: List<Long>)
@Query("UPDATE tasks SET lastNotified = :timestamp WHERE _id = :id AND lastNotified != :timestamp") @Query("UPDATE tasks SET lastNotified = :timestamp WHERE _id = :id AND lastNotified != :timestamp")
abstract suspend fun setLastNotified(id: Long, timestamp: Long): Int abstract suspend fun setLastNotified(id: Long, timestamp: Long)
suspend fun getChildren(id: Long): List<Long> = getChildren(listOf(id)) suspend fun getChildren(id: Long): List<Long> = getChildren(listOf(id))

@ -7,8 +7,10 @@ import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AFTER_DEADLINE import com.todoroo.astrid.data.Task.Companion.NOTIFY_AFTER_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_START import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_START
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_REL_END import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
@ -424,6 +426,12 @@ object Migrations {
database.execSQL( database.execSQL(
"INSERT INTO `alarms` (`task`, `time`, `type`, `repeat`, `interval`) SELECT `_id`, ${HOURS.toMillis(24)}, $TYPE_REL_END, 6, ${HOURS.toMillis(24)} FROM `tasks` WHERE `dueDate` > 0 AND `notificationFlags` | $NOTIFY_AFTER_DEADLINE" "INSERT INTO `alarms` (`task`, `time`, `type`, `repeat`, `interval`) SELECT `_id`, ${HOURS.toMillis(24)}, $TYPE_REL_END, 6, ${HOURS.toMillis(24)} FROM `tasks` WHERE `dueDate` > 0 AND `notificationFlags` | $NOTIFY_AFTER_DEADLINE"
) )
database.execSQL(
"INSERT INTO `alarms` (`task`, `time`, `type`) SELECT `_id`, `task.notifications`, $TYPE_RANDOM FROM `tasks` WHERE `notifications` > 0"
)
database.execSQL(
"INSERT INTO `alarms` (`task`, `time`, `type`) SELECT `_id`, `snoozeTime`, $TYPE_SNOOZE FROM `tasks` WHERE `snoozeTime` > 0"
)
database.execSQL( database.execSQL(
"UPDATE `tasks` SET `notificationFlags` = `notificationFlags` & ~$NOTIFY_AT_START & ~$NOTIFY_AT_DEADLINE & ~$NOTIFY_AFTER_DEADLINE" "UPDATE `tasks` SET `notificationFlags` = `notificationFlags` & ~$NOTIFY_AT_START & ~$NOTIFY_AT_DEADLINE & ~$NOTIFY_AFTER_DEADLINE"
) )

@ -3,71 +3,63 @@ package org.tasks.jobs;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.printTimestamp; import static org.tasks.time.DateTimeUtils.printTimestamp;
import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.notifications.Notification; import org.tasks.notifications.Notification;
public class AlarmEntry implements NotificationQueueEntry { import java.util.Objects;
public class AlarmEntry {
private final long alarmId; private final long alarmId;
private final long taskId; private final long taskId;
private final long time; private final long time;
private final int type;
public AlarmEntry(long alarmId, long taskId, Long time) { public AlarmEntry(long alarmId, long taskId, Long time, int type) {
this.alarmId = alarmId; this.alarmId = alarmId;
this.taskId = taskId; this.taskId = taskId;
this.time = time; this.time = time;
this.type = type;
} }
@Override
public long getId() { public long getId() {
return alarmId; return alarmId;
} }
@Override
public long getTime() { public long getTime() {
return time; return time;
} }
@Override public long getTaskId() {
return taskId;
}
public int getType() {
return type;
}
public Notification toNotification() { public Notification toNotification() {
Notification notification = new Notification(); Notification notification = new Notification();
notification.setTaskId(taskId); notification.setTaskId(taskId);
notification.setType(ReminderService.TYPE_ALARM); notification.setType(type);
notification.setTimestamp(currentTimeMillis()); notification.setTimestamp(currentTimeMillis());
return notification; return notification;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) return true;
return true; if (o == null || getClass() != o.getClass()) return false;
} AlarmEntry that = (AlarmEntry) o;
if (o == null || getClass() != o.getClass()) { return alarmId == that.alarmId && taskId == that.taskId && time == that.time && type == that.type;
return false;
}
AlarmEntry alarmEntry = (AlarmEntry) o;
if (alarmId != alarmEntry.alarmId) {
return false;
}
if (taskId != alarmEntry.taskId) {
return false;
}
return time == alarmEntry.time;
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = (int) (alarmId ^ (alarmId >>> 32)); return Objects.hash(alarmId, taskId, time, type);
result = 31 * result + (int) (taskId ^ (taskId >>> 32));
result = 31 * result + (int) (time ^ (time >>> 32));
return result;
} }
@Override @Override
public String toString() { public String toString() {
return "AlarmEntry{" + "alarmId=" + alarmId + ", taskId=" + taskId + ", time=" + printTimestamp(time) + '}'; return "AlarmEntry{alarmId=" + alarmId + ", taskId=" + taskId + ", time=" + printTimestamp(time) + ", type=" + type + '}';
} }
} }

@ -4,7 +4,6 @@ import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.reminders.ReminderService
import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.timers.TimerPlugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -28,7 +27,6 @@ class CleanupWork @AssistedInject constructor(
private val notificationManager: NotificationManager, private val notificationManager: NotificationManager,
private val geofenceApi: GeofenceApi, private val geofenceApi: GeofenceApi,
private val timerPlugin: TimerPlugin, private val timerPlugin: TimerPlugin,
private val reminderService: ReminderService,
private val alarmService: AlarmService, private val alarmService: AlarmService,
private val taskAttachmentDao: TaskAttachmentDao, private val taskAttachmentDao: TaskAttachmentDao,
private val userActivityDao: UserActivityDao, private val userActivityDao: UserActivityDao,
@ -45,7 +43,6 @@ class CleanupWork @AssistedInject constructor(
runBlocking { runBlocking {
alarmService.cancelAlarms(task) alarmService.cancelAlarms(task)
} }
reminderService.cancelReminder(task)
notificationManager.cancel(task) notificationManager.cancel(task)
locationDao.getGeofencesForTask(task).forEach { locationDao.getGeofencesForTask(task).forEach {
locationDao.delete(it) locationDao.delete(it)

@ -6,7 +6,6 @@ import com.google.common.primitives.Ints
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -16,15 +15,15 @@ class NotificationQueue @Inject constructor(
private val workManager: WorkManager private val workManager: WorkManager
) { ) {
private val jobs = private val jobs =
TreeMultimap.create<Long, NotificationQueueEntry>(Ordering.natural()) { l, r -> TreeMultimap.create<Long, AlarmEntry>(Ordering.natural()) { l, r ->
Ints.compare(l.hashCode(), r.hashCode()) Ints.compare(l.hashCode(), r.hashCode())
} }
@Synchronized @Synchronized
fun <T : NotificationQueueEntry> add(entry: T) = add(listOf(entry)) fun add(entry: AlarmEntry) = add(listOf(entry))
@Synchronized @Synchronized
fun <T : NotificationQueueEntry> add(entries: Iterable<T>) { fun add(entries: Iterable<AlarmEntry>) {
val originalFirstTime = firstTime() val originalFirstTime = firstTime()
entries.forEach { jobs.put(it.time, it) } entries.forEach { jobs.put(it.time, it) }
if (originalFirstTime != firstTime()) { if (originalFirstTime != firstTime()) {
@ -38,27 +37,19 @@ class NotificationQueue @Inject constructor(
workManager.cancelNotifications() workManager.cancelNotifications()
} }
@Synchronized fun cancelForTask(taskId: Long) {
fun cancelAlarm(alarmId: Long) = cancel(AlarmEntry::class.java, alarmId)
@Synchronized
fun cancelReminder(taskId: Long) = cancel(ReminderEntry::class.java, taskId)
private fun cancel(c: Class<out NotificationQueueEntry>, id: Long) {
val firstTime = firstTime() val firstTime = firstTime()
jobs.values() jobs.values().filter { it.taskId == taskId }.forEach { remove(listOf(it)) }
.filter { it.javaClass == c && it.id == id }
.forEach { remove(listOf(it)) }
if (firstTime != firstTime()) { if (firstTime != firstTime()) {
scheduleNext(true) scheduleNext(true)
} }
} }
@get:Synchronized @get:Synchronized
val overdueJobs: List<NotificationQueueEntry> val overdueJobs: List<AlarmEntry>
get() = jobs.keySet() get() = jobs.keySet()
.headSet(DateTime().startOfMinute().plusMinutes(1).millis) .headSet(DateTime().startOfMinute().plusMinutes(1).millis)
.flatMap { jobs[it] } .flatMap { jobs[it] }
@Synchronized @Synchronized
fun scheduleNext() = scheduleNext(false) fun scheduleNext() = scheduleNext(false)
@ -87,10 +78,13 @@ class NotificationQueue @Inject constructor(
fun isEmpty() = jobs.isEmpty fun isEmpty() = jobs.isEmpty
@Synchronized @Synchronized
fun remove(entries: List<NotificationQueueEntry>): Boolean { fun remove(entries: List<AlarmEntry>): Boolean {
var success = true var success = true
for (entry in entries) { for (entry in entries) {
success = success and (!jobs.containsEntry(entry.time, entry) || jobs.remove(entry.time, entry)) success = success and (!jobs.containsEntry(entry.time, entry) || jobs.remove(
entry.time,
entry
))
} }
return success return success
} }

@ -1,12 +0,0 @@
package org.tasks.jobs;
import org.tasks.notifications.Notification;
public interface NotificationQueueEntry {
long getId();
long getTime();
Notification toNotification();
}

@ -3,9 +3,12 @@ package org.tasks.jobs
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.alarms.AlarmService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.Notifier import org.tasks.Notifier
import org.tasks.R import org.tasks.R
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.AlarmDao
import org.tasks.injection.InjectingService import org.tasks.injection.InjectingService
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@ -15,6 +18,8 @@ class NotificationService : InjectingService() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var notifier: Notifier @Inject lateinit var notifier: Notifier
@Inject lateinit var notificationQueue: NotificationQueue @Inject lateinit var notificationQueue: NotificationQueue
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var alarmService: AlarmService
override fun onBind(intent: Intent): IBinder? = null override fun onBind(intent: Intent): IBinder? = null
@ -22,7 +27,6 @@ class NotificationService : InjectingService() {
override val notificationBody = R.string.building_notifications override val notificationBody = R.string.building_notifications
@Synchronized
override suspend fun doWork() { override suspend fun doWork() {
AndroidUtilities.assertNotMainThread() AndroidUtilities.assertNotMainThread()
if (!preferences.isCurrentlyQuietHours) { if (!preferences.isCurrentlyQuietHours) {
@ -31,6 +35,14 @@ class NotificationService : InjectingService() {
throw RuntimeException("Failed to remove jobs from queue") throw RuntimeException("Failed to remove jobs from queue")
} }
notifier.triggerNotifications(overdueJobs.map { it.toNotification() }) notifier.triggerNotifications(overdueJobs.map { it.toNotification() })
overdueJobs
.filter { it.type == TYPE_SNOOZE }
.takeIf { it.isNotEmpty() }
?.map { it.id }
?.let { alarmDao.deleteByIds(it) }
overdueJobs
.map { it.taskId }
.let { alarmService.scheduleAlarms(it) }
} }
} }

@ -1,71 +0,0 @@
package org.tasks.jobs;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.printTimestamp;
import org.tasks.notifications.Notification;
public class ReminderEntry implements NotificationQueueEntry {
private final long taskId;
private final long time;
private final int type;
public ReminderEntry(long taskId, long time, int type) {
this.taskId = taskId;
this.time = time;
this.type = type;
}
@Override
public long getId() {
return taskId;
}
@Override
public long getTime() {
return time;
}
@Override
public Notification toNotification() {
Notification notification = new Notification();
notification.setTaskId(taskId);
notification.setType(type);
notification.setTimestamp(currentTimeMillis());
return notification;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ReminderEntry reminderEntry = (ReminderEntry) o;
if (taskId != reminderEntry.taskId) {
return false;
}
if (time != reminderEntry.time) {
return false;
}
return type == reminderEntry.type;
}
@Override
public int hashCode() {
int result = (int) (taskId ^ (taskId >>> 32));
result = 31 * result + (int) (time ^ (time >>> 32));
result = 31 * result + type;
return result;
}
@Override
public String toString() {
return "ReminderEntry{" + "taskId=" + taskId + ", time=" + printTimestamp(time) + ", type=" + type + '}';
}
}

@ -6,12 +6,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.reminders.ReminderService
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Alarm
import org.tasks.data.LocationDao import org.tasks.data.LocationDao
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.filters.NotificationsFilter import org.tasks.filters.NotificationsFilter
@ -25,7 +24,6 @@ import org.tasks.reminders.SnoozeDialog
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.time.DateTime import org.tasks.time.DateTime
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.min import kotlin.math.min
@ -38,7 +36,6 @@ class NotificationManager @Inject constructor(
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val locationDao: LocationDao, private val locationDao: LocationDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val reminderService: ReminderService,
private val notificationManager: ThrottledNotificationManager, private val notificationManager: ThrottledNotificationManager,
private val markdownProvider: MarkdownProvider, private val markdownProvider: MarkdownProvider,
) { ) {
@ -159,9 +156,7 @@ class NotificationManager @Inject constructor(
if (alert) NotificationCompat.GROUP_ALERT_CHILDREN else NotificationCompat.GROUP_ALERT_SUMMARY) if (alert) NotificationCompat.GROUP_ALERT_CHILDREN else NotificationCompat.GROUP_ALERT_SUMMARY)
notify(notification.taskId, builder, alert, nonstop, fiveTimes) notify(notification.taskId, builder, alert, nonstop, fiveTimes)
val reminderTime = DateTime(notification.timestamp).endOfMinute().millis val reminderTime = DateTime(notification.timestamp).endOfMinute().millis
if (taskDao.setLastNotified(notification.taskId, reminderTime) == 1) { taskDao.setLastNotified(notification.taskId, reminderTime)
reminderService.scheduleAlarm(notification.taskId)
}
alert = false alert = false
} }
} }
@ -295,17 +290,7 @@ class NotificationManager @Inject constructor(
} }
// it's hidden - don't sound, don't delete // it's hidden - don't sound, don't delete
if (task.isHidden && type == ReminderService.TYPE_RANDOM) { if (task.isHidden && type == Alarm.TYPE_RANDOM) {
return null
}
// task due date was changed, but alarm wasn't rescheduled
val dueInFuture = (task.hasDueTime()
&& DateTime(task.dueDate).startOfMinute().millis > DateUtilities.now()
|| !task.hasDueTime()
&& task.dueDate - DateUtilities.now() > DateUtilities.ONE_DAY)
if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE)
&& (!task.hasDueDate() || dueInFuture)) {
return null return null
} }
@ -332,12 +317,12 @@ class NotificationManager @Inject constructor(
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
) )
if (type == ReminderService.TYPE_GEOFENCE_ENTER || type == ReminderService.TYPE_GEOFENCE_EXIT) { if (type == Alarm.TYPE_GEO_ENTER || type == Alarm.TYPE_GEO_EXIT) {
val place = locationDao.getPlace(notification.location!!) val place = locationDao.getPlace(notification.location!!)
if (place != null) { if (place != null) {
builder.setContentText( builder.setContentText(
context.getString( context.getString(
if (type == ReminderService.TYPE_GEOFENCE_ENTER) R.string.location_arrived else R.string.location_departed, if (type == Alarm.TYPE_GEO_ENTER) R.string.location_arrived else R.string.location_departed,
place.displayName)) place.displayName))
} }
} else if (taskDescription?.isNotBlank() == true) { } else if (taskDescription?.isNotBlank() == true) {

@ -42,6 +42,11 @@ class AlarmToString @Inject constructor(
} }
Alarm.TYPE_RANDOM -> Alarm.TYPE_RANDOM ->
resources.getString(R.string.randomly_once) + " " resources.getString(R.string.randomly_once) + " "
Alarm.TYPE_SNOOZE ->
resources.getString(
R.string.snoozed_until,
DateUtilities.getLongDateStringWithTime(alarm.time, locale.locale)
)
else -> else ->
DateUtilities.getLongDateStringWithTime(alarm.time, locale.locale) DateUtilities.getLongDateStringWithTime(alarm.time, locale.locale)
} }

@ -6,25 +6,22 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.reminders.ReminderService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.activities.DateAndTimePickerActivity import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.dialogs.MyTimePickerDialog import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.notifications.NotificationManager
import org.tasks.themes.ThemeAccent import org.tasks.themes.ThemeAccent
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SnoozeActivity : InjectingAppCompatActivity(), SnoozeCallback, DialogInterface.OnCancelListener { class SnoozeActivity : InjectingAppCompatActivity(), SnoozeCallback, DialogInterface.OnCancelListener {
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var reminderService: ReminderService @Inject lateinit var alarmService: AlarmService
@Inject lateinit var themeAccent: ThemeAccent @Inject lateinit var themeAccent: ThemeAccent
private val taskIds: MutableList<Long> = ArrayList() private val taskIds: MutableList<Long> = ArrayList()
@ -61,9 +58,7 @@ class SnoozeActivity : InjectingAppCompatActivity(), SnoozeCallback, DialogInter
override fun snoozeForTime(time: DateTime) { override fun snoozeForTime(time: DateTime) {
lifecycleScope.launch(NonCancellable) { lifecycleScope.launch(NonCancellable) {
taskDao.snooze(taskIds, time.millis) alarmService.snooze(time.millis, taskIds)
reminderService.scheduleAllAlarms(taskIds)
notificationManager.cancel(taskIds)
} }
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()

@ -7,7 +7,6 @@ import android.content.Intent
import android.os.Build import android.os.Build
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.reminders.ReminderService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R import org.tasks.R
@ -21,7 +20,6 @@ import javax.inject.Inject
class NotificationSchedulerIntentService : InjectingJobIntentService() { class NotificationSchedulerIntentService : InjectingJobIntentService() {
@Inject @ApplicationContext lateinit var context: Context @Inject @ApplicationContext lateinit var context: Context
@Inject lateinit var alarmService: AlarmService @Inject lateinit var alarmService: AlarmService
@Inject lateinit var reminderService: ReminderService
@Inject lateinit var notificationQueue: NotificationQueue @Inject lateinit var notificationQueue: NotificationQueue
@Inject lateinit var notificationManager: NotificationManager @Inject lateinit var notificationManager: NotificationManager
@ -31,7 +29,6 @@ class NotificationSchedulerIntentService : InjectingJobIntentService() {
notificationQueue.clear() notificationQueue.clear()
val cancelExistingNotifications = intent.getBooleanExtra(EXTRA_CANCEL_EXISTING_NOTIFICATIONS, false) val cancelExistingNotifications = intent.getBooleanExtra(EXTRA_CANCEL_EXISTING_NOTIFICATIONS, false)
notificationManager.restoreNotifications(cancelExistingNotifications) notificationManager.restoreNotifications(cancelExistingNotifications)
reminderService.scheduleAllAlarms()
alarmService.scheduleAllAlarms() alarmService.scheduleAllAlarms()
} }

@ -270,6 +270,10 @@ public class DateTime {
return add(Calendar.SECOND, seconds); return add(Calendar.SECOND, seconds);
} }
public DateTime plusMillis(int millis) {
return add(Calendar.MILLISECOND, millis);
}
public DateTime minusSeconds(int seconds) { public DateTime minusSeconds(int seconds) {
return subtract(Calendar.SECOND, seconds); return subtract(Calendar.SECOND, seconds);
} }

@ -260,9 +260,6 @@ class TaskEditViewModel @Inject constructor(
var newSubtasks = ArrayList<Task>() var newSubtasks = ArrayList<Task>()
var reminderPeriod: Long? = null
get() = field ?: task?.reminderPeriod ?: 0
var originalAlarms: ImmutableSet<Alarm>? = null var originalAlarms: ImmutableSet<Alarm>? = null
private set(value) { private set(value) {
field = value field = value
@ -315,7 +312,6 @@ class TaskEditViewModel @Inject constructor(
originalLocation != selectedLocation || originalLocation != selectedLocation ||
originalTags?.toHashSet() != selectedTags?.toHashSet() || originalTags?.toHashSet() != selectedTags?.toHashSet() ||
newSubtasks.isNotEmpty() || newSubtasks.isNotEmpty() ||
it.reminderPeriod != reminderPeriod ||
it.ringFlags != getRingFlags() || it.ringFlags != getRingFlags() ||
originalAlarms != selectedAlarms originalAlarms != selectedAlarms
} ?: false } ?: false
@ -342,7 +338,6 @@ class TaskEditViewModel @Inject constructor(
it.elapsedSeconds = elapsedSeconds!! it.elapsedSeconds = elapsedSeconds!!
it.estimatedSeconds = estimatedSeconds!! it.estimatedSeconds = estimatedSeconds!!
it.ringFlags = getRingFlags() it.ringFlags = getRingFlags()
it.reminderPeriod = reminderPeriod!!
applyCalendarChanges() applyCalendarChanges()

@ -721,4 +721,5 @@ File %1$s contained %2$s.\n\n
<string name="alarm_after_start">%s after start</string> <string name="alarm_after_start">%s after start</string>
<string name="alarm_before_due">%s before due</string> <string name="alarm_before_due">%s before due</string>
<string name="alarm_after_due">%s after due</string> <string name="alarm_after_due">%s after due</string>
<string name="snoozed_until">Snoozed until %s</string>
</resources> </resources>

@ -0,0 +1,364 @@
package com.todoroo.astrid.alarms
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.ONE_WEEK
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE_TIME
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.Freeze.Companion.freezeAt
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.Alarm.Companion.whenDue
import org.tasks.data.Alarm.Companion.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.makers.AlarmEntryMaker.TIME
import org.tasks.makers.AlarmEntryMaker.TYPE
import org.tasks.makers.AlarmEntryMaker.newAlarmEntry
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.newTask
import org.tasks.reminders.Random
import org.tasks.time.DateTime
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.DAYS
import java.util.concurrent.TimeUnit.MINUTES
class AlarmCalculatorTest {
private lateinit var random: RandomStub
private lateinit var alarmCalculator: AlarmCalculator
private val now = newDateTime()
@Before
fun setUp() {
random = RandomStub()
alarmCalculator = AlarmCalculator(random) {
TimeUnit.HOURS.toMillis(13).toInt()
}
}
@Test
fun ignoreOldReminder() {
assertNull(
alarmCalculator.toAlarmEntry(
newTask(with(REMINDER_LAST, now)),
Alarm(0L, now.millis, TYPE_DATE_TIME)
)
)
}
@Test
fun dateTimeReminder() {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(REMINDER_LAST, now)),
Alarm(0L, now.millis + 1, TYPE_DATE_TIME)
)
assertEquals(newAlarmEntry(with(TIME, now.plusMillis(1)), with(TYPE, TYPE_DATE_TIME)), alarm)
}
@Test
fun dontIgnoreOldSnooze() {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(REMINDER_LAST, now)),
Alarm(0L, now.millis, TYPE_SNOOZE)
)
assertEquals(
newAlarmEntry(with(TIME, now), with(TYPE, TYPE_SNOOZE)),
alarm
)
}
@Test
fun scheduleReminderAtDefaultDue() {
val alarm = alarmCalculator.toAlarmEntry(newTask(with(DUE_DATE, now)), whenDue(0L))
assertEquals(
newAlarmEntry(
with(TIME, now.startOfDay().withHourOfDay(13)),
with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun scheduleReminderAtDefaultDueTime() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(newTask(with(DUE_TIME, now)), whenDue(0L))
assertEquals(
newAlarmEntry(
with(TIME, now.startOfMinute().plusMillis(1000)), with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun scheduleReminderAtDefaultStart() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_DATE, now), with(HIDE_TYPE, HIDE_UNTIL_DUE)),
whenStarted(0L)
)
assertEquals(
newAlarmEntry(
with(TIME, now.startOfDay().withHourOfDay(13)),
with(TYPE, TYPE_REL_START)
),
alarm
)
}
@Test
fun scheduleReminerAtDefaultStartTime() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now), with(HIDE_TYPE, HIDE_UNTIL_DUE_TIME)),
whenStarted(0L)
)
assertEquals(
newAlarmEntry(
with(TIME, now.startOfMinute().plusMillis(1000)),
with(TYPE, TYPE_REL_START)
),
alarm
)
}
@Test
fun scheduleRelativeAfterDue() {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_DATE, now)),
Alarm(0L, DAYS.toMillis(1), TYPE_REL_END)
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusDays(1).startOfDay().withHourOfDay(13)),
with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun scheduleRelativeAfterDueTime() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now)),
Alarm(0, DAYS.toMillis(1), TYPE_REL_END)
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusDays(1).startOfMinute().plusMillis(1000)),
with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun scheduleRelativeAfterStart() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_DATE, now), with(HIDE_TYPE, HIDE_UNTIL_DUE)),
Alarm(0, DAYS.toMillis(1), TYPE_REL_START)
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusDays(1).startOfDay().withHourOfDay(13)),
with(TYPE, TYPE_REL_START)
),
alarm
)
}
@Test
fun scheduleRelativeAfterStartTime() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now), with(HIDE_TYPE, HIDE_UNTIL_DUE_TIME)),
Alarm(0, DAYS.toMillis(1), TYPE_REL_START)
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusDays(1).startOfMinute().plusMillis(1000)),
with(TYPE, TYPE_REL_START)
),
alarm
)
}
@Test
fun scheduleFirstRepeatReminder() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(4))),
Alarm(0, 0, TYPE_REL_END, 1, MINUTES.toMillis(5))
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusMinutes(5).startOfMinute().plusMillis(1000)),
with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun scheduleSecondRepeatReminder() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(6))),
Alarm(0, 0, TYPE_REL_END, 2, MINUTES.toMillis(5))
)
assertEquals(
newAlarmEntry(
with(TIME, now.plusMinutes(10).startOfMinute().plusMillis(1000)),
with(TYPE, TYPE_REL_END)
),
alarm
)
}
@Test
fun terminateRepeatReminder() = runBlocking {
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, now), with(REMINDER_LAST, now.plusMinutes(10))),
Alarm(0L, 0, TYPE_REL_END, 2, MINUTES.toMillis(5))
)
assertNull(alarm)
}
@Test
fun dontScheduleRelativeEndWithNoEnd() = runBlocking {
assertNull(alarmCalculator.toAlarmEntry(newTask(), whenDue(0L)))
}
@Test
fun dontScheduleRelativeStartWithNoStart() = runBlocking {
assertNull(
alarmCalculator.toAlarmEntry(
newTask(with(DUE_DATE, newDateTime())),
whenStarted(0L)
)
)
}
@Test
fun reminderOverdueEveryDay() = runBlocking {
val dueDate =
Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, DateTime(2022, 1, 30, 13, 30).millis)
.toDateTime()
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, dueDate), with(REMINDER_LAST, dueDate.plusDays(6))),
whenOverdue(0L)
)
assertEquals(
newAlarmEntry(with(TIME, dueDate.plusDays(7)), with(TYPE, TYPE_REL_END)),
alarm
)
}
@Test
fun endDailyOverdueReminder() = runBlocking {
val dueDate =
Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, DateTime(2022, 1, 30, 13, 30).millis)
.toDateTime()
val alarm = alarmCalculator.toAlarmEntry(
newTask(with(DUE_TIME, dueDate), with(REMINDER_LAST, dueDate.plusDays(7))),
whenOverdue(0L)
)
assertNull(alarm)
}
@Test
fun scheduleOverdueRandomReminder() {
random.seed = 0.3865f
freezeAt(now) {
val alarm = alarmCalculator.toAlarmEntry(
newTask(
with(REMINDER_LAST, now.minusDays(14)),
with(CREATION_TIME, now.minusDays(30)),
),
Alarm(0L, ONE_WEEK, TYPE_RANDOM)
)
assertEquals(
newAlarmEntry(with(TIME, now.plusMillis(10148400)), with(TYPE, TYPE_RANDOM)),
alarm
)
}
}
@Test
fun scheduleInitialRandomReminder() {
random.seed = 0.3865f
freezeAt(now) {
val alarm = alarmCalculator.toAlarmEntry(
newTask(
with(REMINDER_LAST, null as DateTime?),
with(CREATION_TIME, now.minusDays(1)),
),
Alarm(0L, ONE_WEEK, TYPE_RANDOM)
)
assertEquals(
newAlarmEntry(
with(TIME, now.minusDays(1).plusMillis(584206592)),
with(TYPE, TYPE_RANDOM)
),
alarm
)
}
}
@Test
fun scheduleNextRandomReminder() {
random.seed = 0.3865f
freezeAt(now) {
val alarm = alarmCalculator.toAlarmEntry(
newTask(
with(REMINDER_LAST, now.minusDays(1)),
with(CREATION_TIME, now.minusDays(30)),
),
Alarm(0L, ONE_WEEK, TYPE_RANDOM)
)
assertEquals(
newAlarmEntry(
with(TIME, now.minusDays(1).plusMillis(584206592)),
with(TYPE, TYPE_RANDOM)
),
alarm
)
}
}
internal class RandomStub : Random() {
var seed = 1.0f
override fun nextFloat() = seed
}
}

@ -10,8 +10,7 @@ import com.todoroo.astrid.service.TaskCompleter
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.`when` import org.mockito.Mockito.*
import org.mockito.Mockito.anyLong
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import org.tasks.time.DateTime import org.tasks.time.DateTime
@ -30,6 +29,7 @@ abstract class RepeatTests {
fun before() { fun before() {
runBlocking { runBlocking {
`when`(alarmService.getAlarms(anyLong())).thenReturn(emptyList()) `when`(alarmService.getAlarms(anyLong())).thenReturn(emptyList())
`when`(alarmService.synchronizeAlarms(anyLong(), anySet())).thenReturn(false)
} }
} }

@ -9,6 +9,7 @@ import org.junit.Test
import org.tasks.TestUtilities.alarms import org.tasks.TestUtilities.alarms
import org.tasks.TestUtilities.vtodo import org.tasks.TestUtilities.vtodo
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.* import java.util.*
@ -84,7 +85,7 @@ class AppleRemindersTests {
@Test @Test
fun dateTimeReminder() { fun dateTimeReminder() {
assertEquals( assertEquals(
listOf(Alarm(0, 1642568400000)), listOf(Alarm(0, 1642568400000, TYPE_DATE_TIME)),
"apple/date_time_reminder.txt".alarms "apple/date_time_reminder.txt".alarms
) )
} }

@ -1,6 +1,5 @@
package org.tasks.jobs package org.tasks.jobs
import com.todoroo.astrid.reminders.ReminderService
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
@ -9,6 +8,8 @@ import org.mockito.AdditionalAnswers
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.Mockito import org.mockito.Mockito
import org.tasks.Freeze.Companion.freezeAt import org.tasks.Freeze.Companion.freezeAt
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils import org.tasks.time.DateTimeUtils
@ -22,7 +23,8 @@ class NotificationQueueTest {
@Before @Before
fun before() { fun before() {
preferences = Mockito.mock(Preferences::class.java) preferences = Mockito.mock(Preferences::class.java)
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong())).then(AdditionalAnswers.returnsFirstArg<Any>()) Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
.then(AdditionalAnswers.returnsFirstArg<Any>())
workManager = Mockito.mock(WorkManager::class.java) workManager = Mockito.mock(WorkManager::class.java)
queue = NotificationQueue(preferences, workManager) queue = NotificationQueue(preferences, workManager)
} }
@ -33,81 +35,52 @@ class NotificationQueueTest {
} }
@Test @Test
fun alarmAndReminderSameTimeSameID() { fun removeAlarmDoesntAffectOtherAlarm() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(AlarmEntry(1, 1, now))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
queue.remove(listOf(AlarmEntry(1, 1, now, TYPE_DATE_TIME)))
freezeAt(now) { freezeAt(now) {
assertEquals( assertEquals(
setOf(AlarmEntry(1, 1, now), ReminderEntry(1, now, ReminderService.TYPE_DUE)), listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)),
queue.overdueJobs.toSet()) queue.overdueJobs
)
} }
} }
@Test @Test
fun alarmAndReminderSameTimeDifferentId() { fun removeByTaskDoesntAffectOtherAlarm() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(AlarmEntry(1, 2, now)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
queue.cancelForTask(1)
freezeAt(now) { freezeAt(now) {
assertEquals( assertEquals(
setOf(AlarmEntry(1, 2, now), ReminderEntry(1, now, ReminderService.TYPE_DUE)), listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)),
queue.overdueJobs.toSet()) queue.overdueJobs
)
} }
} }
@Test
fun removeAlarmLeaveReminder() {
val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE))
queue.add(AlarmEntry(1, 1, now))
Mockito.verify(workManager).scheduleNotification(now)
queue.remove(listOf(AlarmEntry(1, 1, now)))
freezeAt(now) {
assertEquals(
listOf(ReminderEntry(1, now, ReminderService.TYPE_DUE)), queue.overdueJobs)
}
}
@Test
fun removeReminderLeaveAlarm() {
val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE))
queue.add(AlarmEntry(1, 1, now))
Mockito.verify(workManager).scheduleNotification(now)
queue.remove(listOf(ReminderEntry(1, now, ReminderService.TYPE_DUE)))
freezeAt(now) {
assertEquals(listOf(AlarmEntry(1, 1, now)), queue.overdueJobs)
}
}
@Test
fun twoJobsAtSameTime() {
queue.add(ReminderEntry(1, 1, 0))
queue.add(ReminderEntry(2, 1, 0))
Mockito.verify(workManager).scheduleNotification(1)
assertEquals(2, queue.size())
}
@Test @Test
fun rescheduleForFirstJob() { fun rescheduleForFirstJob() {
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 2, 3, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(1) Mockito.verify(workManager).scheduleNotification(3)
} }
@Test @Test
fun dontRescheduleForLaterJobs() { fun dontRescheduleForLaterJobs() {
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 2, 3, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, 2, 0)) queue.add(AlarmEntry(2, 3, 4, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(1) Mockito.verify(workManager).scheduleNotification(3)
} }
@Test @Test
fun rescheduleForNewerJob() { fun rescheduleForNewerJob() {
queue.add(ReminderEntry(1, 2, 0)) queue.add(AlarmEntry(1, 1, 2, TYPE_DATE_TIME))
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 1, 1, TYPE_DATE_TIME))
val order = Mockito.inOrder(workManager) val order = Mockito.inOrder(workManager)
order.verify(workManager).scheduleNotification(2) order.verify(workManager).scheduleNotification(2)
order.verify(workManager).scheduleNotification(1) order.verify(workManager).scheduleNotification(1)
@ -115,8 +88,8 @@ class NotificationQueueTest {
@Test @Test
fun rescheduleWhenCancelingOnlyJob() { fun rescheduleWhenCancelingOnlyJob() {
queue.add(ReminderEntry(1, 2, 0)) queue.add(AlarmEntry(1, 1, 2, TYPE_DATE_TIME))
queue.cancelReminder(1) queue.cancelForTask(1)
val order = Mockito.inOrder(workManager) val order = Mockito.inOrder(workManager)
order.verify(workManager).scheduleNotification(2) order.verify(workManager).scheduleNotification(2)
order.verify(workManager).cancelNotifications() order.verify(workManager).cancelNotifications()
@ -124,9 +97,9 @@ class NotificationQueueTest {
@Test @Test
fun rescheduleWhenCancelingFirstJob() { fun rescheduleWhenCancelingFirstJob() {
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 1, 1, 0))
queue.add(ReminderEntry(2, 2, 0)) queue.add(AlarmEntry(2, 2, 2, 0))
queue.cancelReminder(1) queue.cancelForTask(1)
val order = Mockito.inOrder(workManager) val order = Mockito.inOrder(workManager)
order.verify(workManager).scheduleNotification(1) order.verify(workManager).scheduleNotification(1)
order.verify(workManager).scheduleNotification(2) order.verify(workManager).scheduleNotification(2)
@ -134,95 +107,107 @@ class NotificationQueueTest {
@Test @Test
fun dontRescheduleWhenCancelingLaterJob() { fun dontRescheduleWhenCancelingLaterJob() {
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 1, 1, 0))
queue.add(ReminderEntry(2, 2, 0)) queue.add(AlarmEntry(2, 2, 2, 0))
queue.cancelReminder(2) queue.cancelForTask(2)
Mockito.verify(workManager).scheduleNotification(1) Mockito.verify(workManager).scheduleNotification(1)
} }
@Test @Test
fun nextScheduledTimeIsZeroWhenQueueIsEmpty() { fun nextScheduledTimeIsZeroWhenQueueIsEmpty() {
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong())).thenReturn(1234L) Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
.thenReturn(1234L)
assertEquals(0, queue.nextScheduledTime()) assertEquals(0, queue.nextScheduledTime())
} }
@Test @Test
fun adjustNextScheduledTimeForQuietHours() { fun adjustNextScheduledTimeForQuietHours() {
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong())).thenReturn(1234L) Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
queue.add(ReminderEntry(1, 1, 1)) .thenReturn(1234L)
queue.add(AlarmEntry(1, 1, 1, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(1234) Mockito.verify(workManager).scheduleNotification(1234)
} }
@Test @Test
fun overdueJobsAreReturned() { fun overdueJobsAreReturned() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(2, 1, now + ONE_MINUTE, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
freezeAt(now) { freezeAt(now) {
assertEquals( assertEquals(
listOf(ReminderEntry(1, now, ReminderService.TYPE_DUE)), queue.overdueJobs) listOf(AlarmEntry(1, 1, now, TYPE_DATE_TIME)), queue.overdueJobs
)
} }
} }
@Test @Test
fun twoOverdueJobsAtSameTimeReturned() { fun twoOverdueJobsAtSameTimeReturned() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
freezeAt(now) { freezeAt(now) {
assertEquals( assertEquals(
listOf( setOf(
ReminderEntry(1, now, ReminderService.TYPE_DUE), ReminderEntry(2, now, ReminderService.TYPE_DUE)), AlarmEntry(1, 1, now, TYPE_DATE_TIME),
queue.overdueJobs) AlarmEntry(2, 2, now, TYPE_DATE_TIME)
),
queue.overdueJobs.toSet()
)
} }
} }
@Test @Test
fun twoOverdueJobsAtDifferentTimes() { fun twoOverdueJobsAtDifferentTimes() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
freezeAt(now + 2 * ONE_MINUTE) { freezeAt(now + 2 * ONE_MINUTE) {
assertEquals( assertEquals(
listOf( listOf(
ReminderEntry(1, now, ReminderService.TYPE_DUE), AlarmEntry(1, 1, now, TYPE_DATE_TIME),
ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)), AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME)
queue.overdueJobs) ),
queue.overdueJobs
)
} }
} }
@Test @Test
fun overdueJobsAreRemoved() { fun overdueJobsAreRemoved() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
freezeAt(now) { freezeAt(now) {
queue.remove(queue.overdueJobs) queue.remove(queue.overdueJobs)
} }
assertEquals(listOf(ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)), queue.getJobs()) assertEquals(
listOf(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME)), queue.getJobs()
)
} }
@Test @Test
fun multipleOverduePeriodsLapsed() { fun multipleOverduePeriodsLapsed() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, now + ONE_MINUTE, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
queue.add(ReminderEntry(3, now + 2 * ONE_MINUTE, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(3, 3, now + 2 * ONE_MINUTE, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
freezeAt(now + ONE_MINUTE) { freezeAt(now + ONE_MINUTE) {
queue.remove(queue.overdueJobs) queue.remove(queue.overdueJobs)
} }
assertEquals( assertEquals(
listOf(ReminderEntry(3, now + 2 * ONE_MINUTE, ReminderService.TYPE_DUE)), queue.getJobs()) listOf(AlarmEntry(3, 3, now + 2 * ONE_MINUTE, TYPE_DATE_TIME)), queue.getJobs()
)
} }
@Test @Test
fun clearShouldCancelExisting() { fun clearShouldCancelExisting() {
queue.add(ReminderEntry(1, 1, 0)) queue.add(AlarmEntry(1, 1, 1, 0))
queue.clear() queue.clear()
val order = Mockito.inOrder(workManager) val order = Mockito.inOrder(workManager)
order.verify(workManager).scheduleNotification(1) order.verify(workManager).scheduleNotification(1)
@ -231,10 +216,18 @@ class NotificationQueueTest {
} }
@Test @Test
fun ignoreInvalidCancel() { fun ignoreInvalidCancelForByAlarm() {
val now = DateTimeUtils.currentTimeMillis()
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.remove(listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)))
Mockito.verify(workManager).scheduleNotification(now)
}
@Test
fun ignoreInvalidCancelForTask() {
val now = DateTimeUtils.currentTimeMillis() val now = DateTimeUtils.currentTimeMillis()
queue.add(ReminderEntry(1, now, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
queue.cancelReminder(2) queue.cancelForTask(2)
Mockito.verify(workManager).scheduleNotification(now) Mockito.verify(workManager).scheduleNotification(now)
} }
@ -243,21 +236,24 @@ class NotificationQueueTest {
val now = DateTime(2017, 9, 3, 0, 14, 6, 455) val now = DateTime(2017, 9, 3, 0, 14, 6, 455)
val due = DateTime(2017, 9, 3, 0, 14, 0, 0) val due = DateTime(2017, 9, 3, 0, 14, 0, 0)
val snooze = DateTime(2017, 9, 3, 0, 14, 59, 999) val snooze = DateTime(2017, 9, 3, 0, 14, 59, 999)
queue.add(ReminderEntry(1, due.millis, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(1, 1, due.millis, TYPE_DATE_TIME))
queue.add(ReminderEntry(2, snooze.millis, ReminderService.TYPE_SNOOZE)) queue.add(AlarmEntry(2, 2, snooze.millis, TYPE_SNOOZE))
queue.add(ReminderEntry(3, due.plusMinutes(1).millis, ReminderService.TYPE_DUE)) queue.add(AlarmEntry(3, 3, due.plusMinutes(1).millis, TYPE_DATE_TIME))
Mockito.verify(workManager).scheduleNotification(due.millis) Mockito.verify(workManager).scheduleNotification(due.millis)
freezeAt(now) { freezeAt(now) {
val overdueJobs = queue.overdueJobs val overdueJobs = queue.overdueJobs
assertEquals( assertEquals(
listOf( listOf(
ReminderEntry(1, due.millis, ReminderService.TYPE_DUE), AlarmEntry(1, 1, due.millis, TYPE_DATE_TIME),
ReminderEntry(2, snooze.millis, ReminderService.TYPE_SNOOZE)), AlarmEntry(2, 2, snooze.millis, TYPE_SNOOZE)
overdueJobs) ),
overdueJobs
)
queue.remove(overdueJobs) queue.remove(overdueJobs)
assertEquals( assertEquals(
listOf(ReminderEntry(3, due.plusMinutes(1).millis, ReminderService.TYPE_DUE)), listOf(AlarmEntry(3, 3, due.plusMinutes(1).millis, TYPE_DATE_TIME)),
queue.getJobs()) queue.getJobs()
)
} }
} }

Loading…
Cancel
Save