Convert random and snooze reminders to alarms

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

@ -1,24 +1,21 @@
package com.todoroo.astrid.alarms
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.now
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 com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
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.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted
import org.tasks.data.AlarmDao
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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.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.time.DateTime
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -45,56 +40,20 @@ class AlarmJobServiceTest : InjectingTestCase() {
@Test
fun scheduleAlarm() = runBlocking {
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
fun ignoreStaleAlarm() = runBlocking {
val alarmTime = DateTime(2017, 9, 24, 19, 57)
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()
}
@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
fun dontScheduleReminderForCompletedTask() = runBlocking {
val task = taskDao.insert(
@ -122,107 +81,16 @@ class AlarmJobServiceTest : InjectingTestCase() {
}
@Test
fun scheduleRelativeAfterDue() = 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 {
fun snoozeOverridesAll() = runBlocking {
val now = newDateTime()
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))
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(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 {

@ -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.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
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.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@ -36,6 +36,7 @@ import javax.inject.Inject
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmDao: AlarmDao
@Test
fun getExistingPlace() = runBlocking {
@ -123,8 +124,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
val task = taskDao.createNew(newTask())
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())
}
@ -135,8 +137,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
val task = taskDao.createNew(newTask())
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())
}
@ -147,8 +150,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))
val task = taskDao.createNew(newTask())
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)
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
@ -160,8 +164,9 @@ class LocationDaoTest : InjectingTestCase() {
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))
val task = taskDao.createNew(newTask())
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)
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.parent
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.TagDataDao
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.TaskMaker
import org.tasks.makers.TaskMaker.COLLAPSED
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.util.*
@ -40,6 +42,7 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var alarmDao: AlarmDao
@Test
fun loadRemoteParentInfo() = runBlocking {
@ -211,15 +214,17 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?.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
fun pushSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(
with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30))
))
val taskId = taskDao.createNew(newTask())
alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
@ -237,9 +242,8 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
@Test
fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(
with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30))
))
val taskId = taskDao.createNew(newTask())
alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
@ -261,8 +265,12 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
taskDao.snooze(listOf(task!!.task), 0L)
?: throw IllegalStateException("Missing task")
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()

@ -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_TIME: 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 REMINDERS: Property<Task, Int> = newProperty()
val MODIFICATION_TIME: Property<Task, DateTime> = newProperty()
val CREATION_TIME: Property<Task, DateTime> = newProperty()
val COMPLETION_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 AFTER_COMPLETE: Property<Task, Boolean> = newProperty()
val TITLE: Property<Task, String?> = newProperty()
@ -63,10 +61,6 @@ object TaskMaker {
if (deletedTime != null) {
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)
if (hideType >= 0) {
task.hideUntil = task.createHideUntil(hideType, 0)
@ -79,10 +73,6 @@ object TaskMaker {
if (reminderLast != null) {
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 {
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 org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
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.AlarmDao
import org.tasks.data.TaskDao
import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.withMillisOfDay
import org.tasks.notifications.NotificationManager
import javax.inject.Inject
import javax.inject.Singleton
@ -30,10 +26,10 @@ class AlarmService @Inject constructor(
private val alarmDao: AlarmDao,
private val jobs: NotificationQueue,
private val taskDao: TaskDao,
private val preferences: Preferences,
private val localBroadcastManager: LocalBroadcastManager,
private val notificationManager: NotificationManager,
private val alarmCalculator: AlarmCalculator,
) {
suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
/**
@ -50,8 +46,7 @@ class AlarmService @Inject constructor(
it.time == existing.time &&
it.repeat == existing.repeat &&
it.interval == existing.interval
}) {
jobs.cancelAlarm(existing.id)
}) {
alarmDao.delete(existing)
changed = true
}
@ -68,75 +63,48 @@ class AlarmService @Inject constructor(
return changed
}
private suspend fun getActiveAlarmsForTask(taskId: Long): List<Alarm> =
alarmDao.getActiveAlarms(taskId)
suspend fun scheduleAllAlarms() {
alarmDao
.getActiveAlarms()
.groupBy { it.task }
.forEach { (taskId, alarms) ->
val task = taskDao.fetch(taskId) ?: return@forEach
alarms.forEach { scheduleAlarm(task, it) }
scheduleAlarms(task, alarms)
}
}
suspend fun cancelAlarms(taskId: Long) {
for (alarm in getActiveAlarmsForTask(taskId)) {
jobs.cancelAlarm(alarm.id)
}
fun cancelAlarms(taskId: Long) {
jobs.cancelForTask(taskId)
}
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 */
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 scheduleAlarm(task: Task, alarm: Alarm?) {
if (alarm == null) {
return
}
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))
}
}
private fun scheduleAlarms(task: Task, alarms: List<Alarm>) {
jobs.cancelForTask(task.id)
val alarmEntries = alarms.mapNotNull {
alarmCalculator.toAlarmEntry(task, it)
}
val next =
alarmEntries.find { it.type == TYPE_SNOOZE } ?: alarmEntries.minByOrNull { it.time }
next?.let { jobs.add(it) }
}
companion object {
private const val NO_ALARM = 0L
internal const val NO_ALARM = 0L
}
}

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

@ -84,7 +84,7 @@ class Task : Parcelable {
@SerializedName("ringFlags", alternate = ["reminderFlags"])
var ringFlags = 0
/** Reminder period, in milliseconds. 0 means disabled */
@Deprecated("old random reminders")
@ColumnInfo(name = "notifications")
var reminderPeriod = 0L
@ -92,7 +92,7 @@ class Task : Parcelable {
@ColumnInfo(name = "lastNotified")
var reminderLast = 0L
/** Unixtime snooze is set (0 -> no snooze) */
@Deprecated("old snooze reminders")
@ColumnInfo(name = "snoozeTime")
var reminderSnooze = 0L
@ -143,8 +143,6 @@ class Task : Parcelable {
recurrence = parcel.readString()
ringFlags = parcel.readInt()
reminderLast = parcel.readLong()
reminderPeriod = parcel.readLong()
reminderSnooze = parcel.readLong()
repeatUntil = parcel.readLong()
timerStart = parcel.readLong()
title = parcel.readString()
@ -293,8 +291,6 @@ class Task : Parcelable {
dest.writeString(recurrence)
dest.writeInt(ringFlags)
dest.writeLong(reminderLast)
dest.writeLong(reminderPeriod)
dest.writeLong(reminderSnooze)
dest.writeLong(repeatUntil)
dest.writeLong(timerStart)
dest.writeString(title)
@ -323,13 +319,11 @@ class Task : Parcelable {
&& estimatedSeconds == task.estimatedSeconds
&& elapsedSeconds == task.elapsedSeconds
&& ringFlags == task.ringFlags
&& reminderPeriod == task.reminderPeriod
&& recurrence == task.recurrence
&& repeatUntil == task.repeatUntil
&& calendarURI == task.calendarURI
&& parent == task.parent
&& remoteId == task.remoteId
&& reminderSnooze == task.reminderSnooze
}
fun googleTaskUpToDate(original: Task?): Boolean {
@ -363,7 +357,6 @@ class Task : Parcelable {
&& parent == original.parent
&& repeatUntil == original.repeatUntil
&& isCollapsed == original.isCollapsed
&& reminderSnooze == original.reminderSnooze
}
val isSaved: Boolean
@ -385,6 +378,10 @@ class Task : Parcelable {
putTransitory(TRANS_REMINDERS, flags)
}
var randomReminder: Long
get() = getTransitory(TRANS_RANDOM) ?: 0L
set(value) = putTransitory(TRANS_RANDOM, value)
@Synchronized
fun putTransitory(key: String, value: Any) {
if (transitoryData == null) {
@ -439,9 +436,7 @@ class Task : Parcelable {
if (elapsedSeconds != other.elapsedSeconds) return false
if (timerStart != other.timerStart) return false
if (ringFlags != other.ringFlags) return false
if (reminderPeriod != other.reminderPeriod) return false
if (reminderLast != other.reminderLast) return false
if (reminderSnooze != other.reminderSnooze) return false
if (recurrence != other.recurrence) return false
if (repeatUntil != other.repeatUntil) return false
if (calendarURI != other.calendarURI) return false
@ -468,9 +463,7 @@ class Task : Parcelable {
result = 31 * result + elapsedSeconds
result = 31 * result + timerStart.hashCode()
result = 31 * result + ringFlags
result = 31 * result + reminderPeriod.hashCode()
result = 31 * result + reminderLast.hashCode()
result = 31 * result + reminderSnooze.hashCode()
result = 31 * result + (recurrence?.hashCode() ?: 0)
result = 31 * result + repeatUntil.hashCode()
result = 31 * result + (calendarURI?.hashCode() ?: 0)
@ -482,7 +475,7 @@ class Task : Parcelable {
}
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)
@ -572,6 +565,7 @@ class Task : Parcelable {
private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh"
const val TRANS_REMINDERS = "reminders"
const val TRANS_RANDOM = "random"
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 org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime
@ -67,7 +68,6 @@ class RepeatTaskHelper @Inject constructor(
task.setRecurrence(rrule.toString(), repeatAfterCompletion)
}
task.reminderLast = 0L
task.reminderSnooze = 0L
task.completionDate = 0L
task.setDueDateAdjustingHideUntil(newDueDate)
gcalHelper.rescheduleRepeatingTask(task)
@ -116,13 +116,13 @@ class RepeatTaskHelper @Inject constructor(
return
}
alarmService.getAlarms(taskId)
.takeIf { it.isNotEmpty() }
?.onEach {
.filter { it.type != TYPE_SNOOZE }
.onEach {
if (it.type == Alarm.TYPE_DATE_TIME) {
it.time += newDueDate - oldDueDate
}
}
?.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
}
companion object {

@ -18,6 +18,7 @@ import com.todoroo.astrid.utility.TitleParser.parse
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
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.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted
@ -176,8 +177,10 @@ class TaskCreator @Inject constructor(
companion object {
private fun setDefaultReminders(preferences: Preferences, task: Task) {
task.reminderPeriod = (DateUtilities.ONE_HOUR
* preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, 0))
task.randomReminder = DateUtilities.ONE_HOUR * preferences.getIntegerFromString(
R.string.p_rmd_default_random_hours,
0
)
task.defaultReminders(preferences.defaultReminders)
task.ringFlags = preferences.defaultRingMode
}
@ -197,6 +200,9 @@ class TaskCreator @Inject constructor(
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.gcal.GCalHelper
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.preferences.Preferences
import java.util.*
import javax.inject.Inject
class TaskDuplicator @Inject constructor(
@ -70,7 +79,7 @@ class TaskDuplicator @Inject constructor(
}
val alarms = alarmDao.getAlarms(originalId)
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)
taskDao.save(clone, null) // TODO: delete me

@ -13,6 +13,8 @@ import android.widget.ArrayAdapter
import android.widget.Spinner
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.ui.TaskEditViewModel
/**
@ -39,7 +41,10 @@ internal class RandomReminderControlSet(context: Context, parentView: View, remi
periodSpinner.adapter = adapter
periodSpinner.onItemSelectedListener = object : OnItemSelectedListener {
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<*>?) {}

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

@ -4,12 +4,12 @@ import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
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 dagger.hilt.android.qualifiers.ApplicationContext
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.TaskDao
import org.tasks.intents.TaskIntents
@ -78,7 +78,7 @@ class Notifier @Inject constructor(
.map {
Notification().apply {
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()
location = place
}
@ -92,7 +92,7 @@ class Notifier @Inject constructor(
.filter {
taskDao.fetch(it.taskId)
?.let { task ->
if (it.type != ReminderService.TYPE_RANDOM) {
if (it.type != Alarm.TYPE_RANDOM) {
ringFiveTimes = ringFiveTimes or task.isNotifyModeFive
ringNonstop = ringNonstop or task.isNotifyModeNonstop
}

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

@ -3,8 +3,8 @@ package org.tasks.caldav
import at.bitfire.ical4android.DateUtils.ical4jTimeZone
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.Task.Companion.tasksFromReader
import at.bitfire.ical4android.util.TimeApiExtensions.toDuration
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService
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_TIME
@ -14,23 +14,18 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.ParameterList
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm
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.property.Action
import net.fortuna.ical4j.model.property.Completed
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.Due
import net.fortuna.ical4j.model.property.Geo
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.Trigger
import net.fortuna.ical4j.model.property.XProperty
import org.tasks.Strings.isNullOrEmpty
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.toVAlarms
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
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_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
@ -67,9 +61,6 @@ import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.StringReader
import java.text.ParseException
import java.time.Duration
import java.time.Instant
import java.time.temporal.TemporalAmount
import java.util.*
import javax.inject.Inject
import kotlin.math.max
@ -87,6 +78,7 @@ class iCalendar @Inject constructor(
private val taskDao: TaskDao,
private val caldavDao: CaldavDao,
private val alarmDao: AlarmDao,
private val alarmService: AlarmService,
) {
suspend fun setPlace(taskId: Long, geo: Geo?) {
@ -172,7 +164,9 @@ class iCalendar @Inject constructor(
remoteModel.geoPosition = localGeo
}
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(
@ -191,6 +185,11 @@ class iCalendar @Inject constructor(
task.applyRemote(remote)
setPlace(task.id, remote.geoPosition)
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.suppressRefresh()
taskDao.save(task)
@ -362,7 +361,6 @@ class iCalendar @Inject constructor(
remote.due.apply(this)
remote.dtStart.apply(this)
isCollapsed = remote.collapsed
reminderSnooze = remote.snooze ?: 0
}
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
order = caldavTask.order
collapsed = task.isCollapsed
snooze = task.reminderSnooze
}
val List<VAlarm>.filtered: List<VAlarm>
@ -433,7 +430,9 @@ class iCalendar @Inject constructor(
.filterNot { it.trigger.dateTime == IGNORE_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 {
val tz = ical4jTimeZone(TimeZone.getDefault().id)

@ -46,7 +46,7 @@ class Alarm : Parcelable {
}
@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.time = time
this.type = type
@ -104,6 +104,9 @@ class Alarm : Parcelable {
const val TYPE_REL_START = 1
const val TYPE_REL_END = 2
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)

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

@ -1,10 +1,16 @@
package org.tasks.data
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.astrid.api.FilterListItem.NO_ORDER
import com.todoroo.astrid.data.Task
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.filters.LocationFilters
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.currentTimeMillis
@ -37,14 +43,16 @@ interface LocationDao {
@Query("SELECT geofences.* FROM geofences"
+ " 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"
+ " 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>
@Query("SELECT geofences.* FROM geofences"
+ " 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"
+ " 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>
@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)")
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 "
+ "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)"
@ -78,10 +75,6 @@ abstract class TaskDao(private val database: Database) {
ORDER BY created""")
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
@Query("SELECT * FROM tasks")
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>)
@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))

@ -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_AT_DEADLINE
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_START
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import timber.log.Timber
import java.util.concurrent.TimeUnit.HOURS
@ -424,6 +426,12 @@ object Migrations {
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"
)
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(
"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.printTimestamp;
import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.notifications.Notification;
public class AlarmEntry implements NotificationQueueEntry {
import java.util.Objects;
public class AlarmEntry {
private final long alarmId;
private final long taskId;
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.taskId = taskId;
this.time = time;
this.type = type;
}
@Override
public long getId() {
return alarmId;
}
@Override
public long getTime() {
return time;
}
@Override
public long getTaskId() {
return taskId;
}
public int getType() {
return type;
}
public Notification toNotification() {
Notification notification = new Notification();
notification.setTaskId(taskId);
notification.setType(ReminderService.TYPE_ALARM);
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;
}
AlarmEntry alarmEntry = (AlarmEntry) o;
if (alarmId != alarmEntry.alarmId) {
return false;
}
if (taskId != alarmEntry.taskId) {
return false;
}
return time == alarmEntry.time;
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AlarmEntry that = (AlarmEntry) o;
return alarmId == that.alarmId && taskId == that.taskId && time == that.time && type == that.type;
}
@Override
public int hashCode() {
int result = (int) (alarmId ^ (alarmId >>> 32));
result = 31 * result + (int) (taskId ^ (taskId >>> 32));
result = 31 * result + (int) (time ^ (time >>> 32));
return result;
return Objects.hash(alarmId, taskId, time, type);
}
@Override
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.work.WorkerParameters
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.reminders.ReminderService
import com.todoroo.astrid.timers.TimerPlugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -28,7 +27,6 @@ class CleanupWork @AssistedInject constructor(
private val notificationManager: NotificationManager,
private val geofenceApi: GeofenceApi,
private val timerPlugin: TimerPlugin,
private val reminderService: ReminderService,
private val alarmService: AlarmService,
private val taskAttachmentDao: TaskAttachmentDao,
private val userActivityDao: UserActivityDao,
@ -45,7 +43,6 @@ class CleanupWork @AssistedInject constructor(
runBlocking {
alarmService.cancelAlarms(task)
}
reminderService.cancelReminder(task)
notificationManager.cancel(task)
locationDao.getGeofencesForTask(task).forEach {
locationDao.delete(it)

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

@ -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.os.IBinder
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.alarms.AlarmService
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.Notifier
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.preferences.Preferences
import javax.inject.Inject
@ -15,6 +18,8 @@ class NotificationService : InjectingService() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var notifier: Notifier
@Inject lateinit var notificationQueue: NotificationQueue
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var alarmService: AlarmService
override fun onBind(intent: Intent): IBinder? = null
@ -22,7 +27,6 @@ class NotificationService : InjectingService() {
override val notificationBody = R.string.building_notifications
@Synchronized
override suspend fun doWork() {
AndroidUtilities.assertNotMainThread()
if (!preferences.isCurrentlyQuietHours) {
@ -31,6 +35,14 @@ class NotificationService : InjectingService() {
throw RuntimeException("Failed to remove jobs from queue")
}
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 androidx.core.app.NotificationCompat
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 org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Alarm
import org.tasks.data.LocationDao
import org.tasks.data.TaskDao
import org.tasks.filters.NotificationsFilter
@ -25,7 +24,6 @@ import org.tasks.reminders.SnoozeDialog
import org.tasks.themes.ColorProvider
import org.tasks.time.DateTime
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.min
@ -38,7 +36,6 @@ class NotificationManager @Inject constructor(
private val taskDao: TaskDao,
private val locationDao: LocationDao,
private val localBroadcastManager: LocalBroadcastManager,
private val reminderService: ReminderService,
private val notificationManager: ThrottledNotificationManager,
private val markdownProvider: MarkdownProvider,
) {
@ -159,9 +156,7 @@ class NotificationManager @Inject constructor(
if (alert) NotificationCompat.GROUP_ALERT_CHILDREN else NotificationCompat.GROUP_ALERT_SUMMARY)
notify(notification.taskId, builder, alert, nonstop, fiveTimes)
val reminderTime = DateTime(notification.timestamp).endOfMinute().millis
if (taskDao.setLastNotified(notification.taskId, reminderTime) == 1) {
reminderService.scheduleAlarm(notification.taskId)
}
taskDao.setLastNotified(notification.taskId, reminderTime)
alert = false
}
}
@ -295,17 +290,7 @@ class NotificationManager @Inject constructor(
}
// it's hidden - don't sound, don't delete
if (task.isHidden && type == ReminderService.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)) {
if (task.isHidden && type == Alarm.TYPE_RANDOM) {
return null
}
@ -332,12 +317,12 @@ class NotificationManager @Inject constructor(
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!!)
if (place != null) {
builder.setContentText(
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))
}
} else if (taskDescription?.isNotBlank() == true) {

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

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

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

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

@ -260,9 +260,6 @@ class TaskEditViewModel @Inject constructor(
var newSubtasks = ArrayList<Task>()
var reminderPeriod: Long? = null
get() = field ?: task?.reminderPeriod ?: 0
var originalAlarms: ImmutableSet<Alarm>? = null
private set(value) {
field = value
@ -315,7 +312,6 @@ class TaskEditViewModel @Inject constructor(
originalLocation != selectedLocation ||
originalTags?.toHashSet() != selectedTags?.toHashSet() ||
newSubtasks.isNotEmpty() ||
it.reminderPeriod != reminderPeriod ||
it.ringFlags != getRingFlags() ||
originalAlarms != selectedAlarms
} ?: false
@ -342,7 +338,6 @@ class TaskEditViewModel @Inject constructor(
it.elapsedSeconds = elapsedSeconds!!
it.estimatedSeconds = estimatedSeconds!!
it.ringFlags = getRingFlags()
it.reminderPeriod = reminderPeriod!!
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_before_due">%s before due</string>
<string name="alarm_after_due">%s after due</string>
<string name="snoozed_until">Snoozed until %s</string>
</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 org.junit.Before
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.*
import org.tasks.LocalBroadcastManager
import org.tasks.makers.TaskMaker
import org.tasks.time.DateTime
@ -30,6 +29,7 @@ abstract class RepeatTests {
fun before() {
runBlocking {
`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.vtodo
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.time.DateTime
import java.util.*
@ -84,7 +85,7 @@ class AppleRemindersTests {
@Test
fun dateTimeReminder() {
assertEquals(
listOf(Alarm(0, 1642568400000)),
listOf(Alarm(0, 1642568400000, TYPE_DATE_TIME)),
"apple/date_time_reminder.txt".alarms
)
}

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

Loading…
Cancel
Save