Add support for relative and repeating alarms

pull/1765/head
Alex Baker 2 years ago
parent 861a82baf4
commit 2d9c1638dc

File diff suppressed because it is too large Load Diff

@ -1,22 +1,37 @@
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.astrid.dao.TaskDao 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.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.Alarm import org.tasks.data.Alarm
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.AlarmDao
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
import org.tasks.jobs.NotificationQueue import org.tasks.jobs.NotificationQueue
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.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)
@ -29,24 +44,195 @@ class AlarmJobServiceTest : InjectingTestCase() {
@Test @Test
fun scheduleAlarm() = runBlocking { fun scheduleAlarm() = runBlocking {
val task = newTask() val task = taskDao.createNew(newTask())
taskDao.createNew(task) val alarm = insertAlarm(Alarm(task, DateTime(2017, 9, 24, 19, 57).millis))
val alarmTime = DateTime(2017, 9, 24, 19, 57)
val alarm = Alarm(task.id, alarmTime.millis)
alarm.id = alarmDao.insert(alarm)
alarmService.scheduleAllAlarms()
assertEquals(listOf(AlarmEntry(alarm)), jobs.getJobs()) verify(AlarmEntry(alarm, task, DateTime(2017, 9, 24, 19, 57).millis))
} }
@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 = newTask(with(REMINDER_LAST, alarmTime.endOfMinute())) val task = taskDao.createNew(newTask(with(REMINDER_LAST, alarmTime.endOfMinute())))
taskDao.createNew(task) alarmDao.insert(Alarm(task, alarmTime.millis))
alarmDao.insert(Alarm(task.id, alarmTime.millis))
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(
newTask(
with(DUE_DATE, newDateTime()),
with(COMPLETION_TIME, newDateTime())
)
)
alarmDao.insert(whenDue(task))
verify()
}
@Test
fun dontScheduleReminderForDeletedTask() = runBlocking {
val task = taskDao.insert(
newTask(
with(DUE_DATE, newDateTime()),
with(DELETION_TIME, newDateTime())
)
)
alarmDao.insert(whenDue(task))
verify()
}
@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 {
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))
verify()
}
private suspend fun insertAlarm(alarm: Alarm): Long {
alarm.id = alarmDao.insert(alarm)
return alarm.id
}
private suspend fun verify(vararg alarms: AlarmEntry) {
alarmService.scheduleAllAlarms() alarmService.scheduleAllAlarms()
assertTrue(jobs.getJobs().isEmpty()) assertEquals(alarms.toList(), jobs.getJobs())
} }
} }

@ -3,7 +3,6 @@ package com.todoroo.astrid.reminders
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE
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 org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -11,19 +10,14 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.Freeze.Companion.freezeClock import org.tasks.Freeze.Companion.freezeClock
import org.tasks.R
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
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
import org.tasks.jobs.NotificationQueue import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry import org.tasks.jobs.ReminderEntry
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.CREATION_TIME import org.tasks.makers.TaskMaker.CREATION_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.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD import org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD
import org.tasks.makers.TaskMaker.REMINDERS import org.tasks.makers.TaskMaker.REMINDERS
@ -33,7 +27,6 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.reminders.Random import org.tasks.reminders.Random
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)
@ -51,152 +44,7 @@ class ReminderServiceTest : InjectingTestCase() {
super.setUp() super.setUp()
random = RandomStub() random = RandomStub()
preferences.clear() preferences.clear()
service = ReminderService(preferences, jobs, random, taskDao) service = ReminderService(jobs, random, taskDao)
}
@Test
fun dontScheduleStartDateReminderWhenFlagNotSet() {
service.scheduleAlarm(
newTask(
with(ID, 1L),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(DUE_TIME, newDateTime())
)
)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenFlagNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(DUE_TIME, newDateTime())))
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenTimeNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(REMINDERS, Task.NOTIFY_AT_DEADLINE)))
assertTrue(jobs.isEmpty())
}
@Test
fun schedulePastStartDate() {
freezeClock {
val dueDate = newDateTime().minusDays(1)
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(HIDE_TYPE, HIDE_UNTIL_DUE),
with(REMINDERS, Task.NOTIFY_AT_START)
)
service.scheduleAlarm(task)
verify(
ReminderEntry(
1,
dueDate.startOfDay().withHourOfDay(18).millis,
ReminderService.TYPE_START
)
)
}
}
@Test
fun scheduleFutureStartDate() {
val dueDate = newDateTime().plusDays(1)
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(HIDE_TYPE, HIDE_UNTIL_DUE),
with(REMINDERS, Task.NOTIFY_AT_START)
)
service.scheduleAlarm(task)
verify(
ReminderEntry(
1,
dueDate.startOfDay().withHourOfDay(18).millis,
ReminderService.TYPE_START
)
)
}
@Test
fun schedulePastDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().minusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleFutureDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleReminderAtDefaultDueTime() {
val now = newDateTime()
val task = newTask(with(ID, 1L), with(DUE_DATE, now), with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.startOfDay().withHourOfDay(18).millis, ReminderService.TYPE_DUE))
}
@Test
fun dontScheduleReminderForCompletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(COMPLETION_TIME, newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleReminderForDeletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(DELETION_TIME, newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenAlreadyReminded() {
val now = newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(REMINDER_LAST, now.plusSeconds(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
} }
@Test @Test
@ -205,12 +53,12 @@ class ReminderServiceTest : InjectingTestCase() {
with(ID, 1L), with(ID, 1L),
with(DUE_TIME, newDateTime()), with(DUE_TIME, newDateTime()),
with(SNOOZE_TIME, newDateTime().minusMinutes(5)), with(SNOOZE_TIME, newDateTime().minusMinutes(5)),
with(REMINDER_LAST, newDateTime().minusMinutes(4)), with(REMINDER_LAST, newDateTime().minusMinutes(4))
with(REMINDERS, Task.NOTIFY_AT_DEADLINE)) )
service.scheduleAlarm(task) service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE)) assertTrue(jobs.isEmpty())
} }
@Test @Test
@ -282,84 +130,6 @@ class ReminderServiceTest : InjectingTestCase() {
} }
} }
@Test
fun scheduleOverdueNoLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, null as DateTime?),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverduePastLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 12, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueBeforeLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 12, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 15, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 25, 12, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueWithNoDueTime() {
preferences.setInt(R.string.p_rmd_time, TimeUnit.HOURS.toMillis(15).toInt())
val task = newTask(
with(ID, 1L),
with(DUE_DATE, DateTime(2017, 9, 22)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 0, 0, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleSubsequentOverdueReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 15, 30, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueAfterLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test @Test
fun snoozeOverridesAll() { fun snoozeOverridesAll() {

@ -1,39 +1,66 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.todoroo.astrid.data.Task
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.assertFalse import org.junit.Assert.*
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.whenOverdue
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils.currentTimeMillis
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ReminderTests : BaseTaskEditViewModelTest() { class ReminderTests : BaseTaskEditViewModelTest() {
@Test
fun whenStartReminder() = runBlocking {
val task = newTask()
task.defaultReminders(Task.NOTIFY_AT_START)
setup(task)
viewModel.hideUntil = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, currentTimeMillis())
save()
assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_START).apply { id = 1 }),
alarmDao.getAlarms(task.id)
)
}
@Test @Test
fun whenDueReminder() = runBlocking { fun whenDueReminder() = runBlocking {
val task = newTask() val task = newTask()
task.defaultReminders(Task.NOTIFY_AT_DEADLINE)
setup(task) setup(task)
viewModel.whenDue = true viewModel.dueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, currentTimeMillis())
save() save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyAtDeadline) assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_END).apply { id = 1 }),
alarmDao.getAlarms(task.id)
)
} }
@Test @Test
fun whenOverDueReminder() = runBlocking { fun whenOverDueReminder() = runBlocking {
val task = newTask() val task = newTask()
task.defaultReminders(Task.NOTIFY_AFTER_DEADLINE)
setup(task) setup(task)
viewModel.whenOverdue = true viewModel.dueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, currentTimeMillis())
save() save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyAfterDeadline) assertEquals(
listOf(whenOverdue(1).apply { id = 1 }),
alarmDao.getAlarms(task.id)
)
} }
@Test @Test

@ -73,7 +73,7 @@ object TaskMaker {
} }
val reminderFlags = lookup.valueOf(REMINDERS, -1) val reminderFlags = lookup.valueOf(REMINDERS, -1)
if (reminderFlags >= 0) { if (reminderFlags >= 0) {
task.reminderFlags = reminderFlags task.ringFlags = reminderFlags
} }
val reminderLast = lookup.valueOf(REMINDER_LAST, null as DateTime?) val reminderLast = lookup.valueOf(REMINDER_LAST, null as DateTime?)
if (reminderLast != null) { if (reminderLast != null) {

@ -5,10 +5,17 @@
*/ */
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import com.todoroo.astrid.data.Task
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_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.jobs.AlarmEntry import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue import org.tasks.jobs.NotificationQueue
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.withMillisOfDay
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -20,7 +27,10 @@ import javax.inject.Singleton
@Singleton @Singleton
class AlarmService @Inject constructor( 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 preferences: Preferences,
) {
suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId) suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
@ -30,9 +40,15 @@ class AlarmService @Inject constructor(
* @return true if data was changed * @return true if data was changed
*/ */
suspend fun synchronizeAlarms(taskId: Long, alarms: MutableSet<Alarm>): Boolean { suspend fun synchronizeAlarms(taskId: Long, alarms: MutableSet<Alarm>): Boolean {
val task = taskDao.fetch(taskId) ?: return false
var changed = false var changed = false
for (existing in alarmDao.getAlarms(taskId)) { for (existing in alarmDao.getAlarms(taskId)) {
if (!alarms.removeIf { it.time == existing.time }) { if (!alarms.removeIf {
it.type == existing.type &&
it.time == existing.time &&
it.repeat == existing.repeat &&
it.interval == existing.interval
}) {
jobs.cancelAlarm(existing.id) jobs.cancelAlarm(existing.id)
alarmDao.delete(existing) alarmDao.delete(existing)
changed = true changed = true
@ -44,7 +60,7 @@ class AlarmService @Inject constructor(
changed = true changed = true
} }
if (changed) { if (changed) {
scheduleAlarms(taskId) scheduleAlarms(task)
} }
return changed return changed
} }
@ -53,7 +69,13 @@ class AlarmService @Inject constructor(
alarmDao.getActiveAlarms(taskId) alarmDao.getActiveAlarms(taskId)
suspend fun scheduleAllAlarms() { suspend fun scheduleAllAlarms() {
alarmDao.getActiveAlarms().forEach(::scheduleAlarm) alarmDao
.getActiveAlarms()
.groupBy { it.task }
.forEach { (taskId, alarms) ->
val task = taskDao.fetch(taskId) ?: return@forEach
alarms.forEach { scheduleAlarm(task, it) }
}
} }
suspend fun cancelAlarms(taskId: Long) { suspend fun cancelAlarms(taskId: Long) {
@ -63,25 +85,55 @@ class AlarmService @Inject constructor(
} }
/** Schedules alarms for a single task */ /** Schedules alarms for a single task */
private suspend fun scheduleAlarms(taskId: Long) { suspend fun scheduleAlarms(task: Task) {
getActiveAlarmsForTask(taskId).forEach(::scheduleAlarm) getActiveAlarmsForTask(task.id).forEach { scheduleAlarm(task, it) }
} }
/** Schedules alarms for a single task */ /** Schedules alarms for a single task */
private fun scheduleAlarm(alarm: Alarm?) { private fun scheduleAlarm(task: Task, alarm: Alarm?) {
if (alarm == null) { if (alarm == null) {
return return
} }
val alarmEntry = AlarmEntry(alarm) val trigger = when (alarm.type) {
val time = alarmEntry.time TYPE_DATE_TIME ->
if (time == 0L || time == NO_ALARM) { alarm.time
jobs.cancelAlarm(alarmEntry.id) TYPE_REL_START ->
} else { when {
jobs.add(alarmEntry) 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))
}
}
} }
} }
companion object { companion object {
private const val NO_ALARM = Long.MAX_VALUE private const val NO_ALARM = 0L
} }
} }

@ -61,7 +61,8 @@ import org.tasks.notifications.NotificationDao
Principal::class, Principal::class,
PrincipalAccess::class PrincipalAccess::class
], ],
version = 80) version = 81
)
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao abstract fun notificationDao(): NotificationDao
abstract val tagDataDao: TagDataDao abstract val tagDataDao: TagDataDao

@ -6,6 +6,7 @@
package com.todoroo.astrid.dao package com.todoroo.astrid.dao
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities.now
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.reminders.ReminderService
@ -34,7 +35,9 @@ class TaskDao @Inject 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 syncAdapters: SyncAdapters) { private val syncAdapters: SyncAdapters,
private val alarmService: AlarmService,
) {
suspend fun fetch(id: Long): Task? = taskDao.fetch(id) suspend fun fetch(id: Long): Task? = taskDao.fetch(id)
@ -135,6 +138,7 @@ class TaskDao @Inject constructor(
geofenceApi.update(task.id) geofenceApi.update(task.id)
} }
reminderService.scheduleAlarm(task) reminderService.scheduleAlarm(task)
alarmService.scheduleAlarms(task)
refreshScheduler.scheduleRefresh(task) refreshScheduler.scheduleRefresh(task)
if (!task.isSuppressRefresh()) { if (!task.isSuppressRefresh()) {
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefresh()

@ -5,7 +5,12 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.IntDef import androidx.annotation.IntDef
import androidx.core.os.ParcelCompat import androidx.core.os.ParcelCompat
import androidx.room.* import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.todoroo.andlib.data.Table import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
@ -17,7 +22,6 @@ import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfDay
import timber.log.Timber import timber.log.Timber
import java.util.*
@Entity( @Entity(
tableName = Task.TABLE_NAME, tableName = Task.TABLE_NAME,
@ -77,7 +81,8 @@ class Task : Parcelable {
/** Flags for when to send reminders */ /** Flags for when to send reminders */
@ColumnInfo(name = "notificationFlags") @ColumnInfo(name = "notificationFlags")
var reminderFlags = 0 @SerializedName("ringFlags", alternate = ["reminderFlags"])
var ringFlags = 0
/** Reminder period, in milliseconds. 0 means disabled */ /** Reminder period, in milliseconds. 0 means disabled */
@ColumnInfo(name = "notifications") @ColumnInfo(name = "notifications")
@ -136,7 +141,7 @@ class Task : Parcelable {
modificationDate = parcel.readLong() modificationDate = parcel.readLong()
notes = parcel.readString() notes = parcel.readString()
recurrence = parcel.readString() recurrence = parcel.readString()
reminderFlags = parcel.readInt() ringFlags = parcel.readInt()
reminderLast = parcel.readLong() reminderLast = parcel.readLong()
reminderPeriod = parcel.readLong() reminderPeriod = parcel.readLong()
reminderSnooze = parcel.readLong() reminderSnooze = parcel.readLong()
@ -243,22 +248,26 @@ class Task : Parcelable {
} }
val isNotifyModeNonstop: Boolean val isNotifyModeNonstop: Boolean
get() = isReminderFlagSet(NOTIFY_MODE_NONSTOP) get() = isRingSet(NOTIFY_MODE_NONSTOP)
val isNotifyModeFive: Boolean val isNotifyModeFive: Boolean
get() = isReminderFlagSet(NOTIFY_MODE_FIVE) get() = isRingSet(NOTIFY_MODE_FIVE)
val isNotifyAfterDeadline: Boolean val isNotifyAfterDeadline: Boolean
get() = isReminderFlagSet(NOTIFY_AFTER_DEADLINE) get() = isReminderSet(NOTIFY_AFTER_DEADLINE)
val isNotifyAtStart: Boolean val isNotifyAtStart: Boolean
get() = isReminderFlagSet(NOTIFY_AT_START) get() = isReminderSet(NOTIFY_AT_START)
val isNotifyAtDeadline: Boolean val isNotifyAtDeadline: Boolean
get() = isReminderFlagSet(NOTIFY_AT_DEADLINE) get() = isReminderSet(NOTIFY_AT_DEADLINE)
private fun isReminderFlagSet(flag: Int): Boolean { private fun isReminderSet(flag: Int): Boolean {
return reminderFlags and flag > 0 return ((transitoryData?.get(TRANS_REMINDERS) as? Int) ?: 0) and flag > 0
}
private fun isRingSet(flag: Int): Boolean {
return ringFlags and flag > 0
} }
val isNew: Boolean val isNew: Boolean
@ -282,7 +291,7 @@ class Task : Parcelable {
dest.writeLong(modificationDate) dest.writeLong(modificationDate)
dest.writeString(notes) dest.writeString(notes)
dest.writeString(recurrence) dest.writeString(recurrence)
dest.writeInt(reminderFlags) dest.writeInt(ringFlags)
dest.writeLong(reminderLast) dest.writeLong(reminderLast)
dest.writeLong(reminderPeriod) dest.writeLong(reminderPeriod)
dest.writeLong(reminderSnooze) dest.writeLong(reminderSnooze)
@ -313,7 +322,7 @@ class Task : Parcelable {
&& notes == task.notes && notes == task.notes
&& estimatedSeconds == task.estimatedSeconds && estimatedSeconds == task.estimatedSeconds
&& elapsedSeconds == task.elapsedSeconds && elapsedSeconds == task.elapsedSeconds
&& reminderFlags == task.reminderFlags && ringFlags == task.ringFlags
&& reminderPeriod == task.reminderPeriod && reminderPeriod == task.reminderPeriod
&& recurrence == task.recurrence && recurrence == task.recurrence
&& repeatUntil == task.repeatUntil && repeatUntil == task.repeatUntil
@ -372,6 +381,10 @@ class Task : Parcelable {
fun isSuppressRefresh() = checkTransitory(TRANS_SUPPRESS_REFRESH) fun isSuppressRefresh() = checkTransitory(TRANS_SUPPRESS_REFRESH)
fun defaultReminders(flags: Int) {
putTransitory(TRANS_REMINDERS, flags)
}
@Synchronized @Synchronized
fun putTransitory(key: String, value: Any) { fun putTransitory(key: String, value: Any) {
if (transitoryData == null) { if (transitoryData == null) {
@ -425,7 +438,7 @@ class Task : Parcelable {
if (estimatedSeconds != other.estimatedSeconds) return false if (estimatedSeconds != other.estimatedSeconds) return false
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 (reminderFlags != other.reminderFlags) return false if (ringFlags != other.ringFlags) return false
if (reminderPeriod != other.reminderPeriod) 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 (reminderSnooze != other.reminderSnooze) return false
@ -454,7 +467,7 @@ class Task : Parcelable {
result = 31 * result + estimatedSeconds result = 31 * result + estimatedSeconds
result = 31 * result + elapsedSeconds result = 31 * result + elapsedSeconds
result = 31 * result + timerStart.hashCode() result = 31 * result + timerStart.hashCode()
result = 31 * result + reminderFlags result = 31 * result + ringFlags
result = 31 * result + reminderPeriod.hashCode() result = 31 * result + reminderPeriod.hashCode()
result = 31 * result + reminderLast.hashCode() result = 31 * result + reminderLast.hashCode()
result = 31 * result + reminderSnooze.hashCode() result = 31 * result + reminderSnooze.hashCode()
@ -469,7 +482,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, reminderFlags=$reminderFlags, 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, reminderPeriod=$reminderPeriod, reminderLast=$reminderLast, reminderSnooze=$reminderSnooze, recurrence=$recurrence, repeatUntil=$repeatUntil, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData)"
} }
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@ -558,6 +571,7 @@ class Task : Parcelable {
const val URGENCY_IN_TWO_WEEKS = 5 const val URGENCY_IN_TWO_WEEKS = 5
private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh" private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh"
const val TRANS_REMINDERS = "reminders"
private val INVALID_COUNT = ";?COUNT=-1".toRegex() private val INVALID_COUNT = ";?COUNT=-1".toRegex()

@ -10,25 +10,22 @@ import com.todoroo.astrid.data.Task
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.jobs.NotificationQueue import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry import org.tasks.jobs.ReminderEntry
import org.tasks.preferences.Preferences
import org.tasks.reminders.Random import org.tasks.reminders.Random
import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class ReminderService internal constructor( class ReminderService internal constructor(
private val preferences: Preferences,
private val jobs: NotificationQueue, private val jobs: NotificationQueue,
private val random: Random, private val random: Random,
private val taskDao: TaskDao) { private val taskDao: TaskDao,
) {
@Inject @Inject
internal constructor( internal constructor(
preferences: Preferences,
notificationQueue: NotificationQueue, notificationQueue: NotificationQueue,
taskDao: TaskDao taskDao: TaskDao
) : this(preferences, notificationQueue, Random(), taskDao) ) : this(notificationQueue, Random(), taskDao)
suspend fun scheduleAlarm(id: Long) = scheduleAllAlarms(listOf(id)) suspend fun scheduleAlarm(id: Long) = scheduleAllAlarms(listOf(id))
@ -66,31 +63,12 @@ class ReminderService internal constructor(
// random reminders // random reminders
val whenRandom = calculateNextRandomReminder(task) val whenRandom = calculateNextRandomReminder(task)
val whenStartDate = calculateStartDateReminder(task)
// notifications at due date
val whenDueDate = calculateNextDueDateReminder(task)
// notifications after due date
val whenOverdue = calculateNextOverdueReminder(task)
// snooze trumps all // snooze trumps all
if (whenSnooze != NO_ALARM) { return when {
return ReminderEntry(taskId, whenSnooze, TYPE_SNOOZE) whenSnooze != NO_ALARM -> ReminderEntry(taskId, whenSnooze, TYPE_SNOOZE)
} else if ( whenRandom != NO_ALARM -> ReminderEntry(taskId, whenRandom, TYPE_RANDOM)
whenRandom < whenDueDate && else -> null
whenRandom < whenOverdue &&
whenRandom < whenStartDate
) {
return ReminderEntry(taskId, whenRandom, TYPE_RANDOM)
} else if (whenStartDate < whenDueDate) {
return ReminderEntry(taskId, whenStartDate, TYPE_START)
} else if (whenDueDate < whenOverdue) {
return ReminderEntry(taskId, whenDueDate, TYPE_DUE)
} else if (whenOverdue != NO_ALARM) {
return ReminderEntry(taskId, whenOverdue, TYPE_OVERDUE)
} }
return null
} }
private fun calculateNextSnoozeReminder(task: Task): Long { private fun calculateNextSnoozeReminder(task: Task): Long {
@ -99,64 +77,6 @@ class ReminderService internal constructor(
} else NO_ALARM } else NO_ALARM
} }
private fun calculateNextOverdueReminder(task: Task): Long {
// Uses getNowValue() instead of DateUtilities.now()
if (task.hasDueDate() && task.isNotifyAfterDeadline) {
var overdueDate = DateTime(task.dueDate).plusDays(1)
if (!task.hasDueTime()) {
overdueDate = overdueDate.withMillisOfDay(preferences.defaultDueTime)
}
val lastReminder = DateTime(task.reminderLast)
if (overdueDate.isAfter(lastReminder)) {
return overdueDate.millis
}
overdueDate = lastReminder.withMillisOfDay(overdueDate.millisOfDay)
return if (overdueDate.isAfter(lastReminder)) overdueDate.millis else overdueDate.plusDays(1).millis
}
return NO_ALARM
}
private fun calculateStartDateReminder(task: Task): Long {
if (task.hasStartDate() && task.isNotifyAtStart) {
val startDate = task.hideUntil
val startDateAlarm = if (task.hasStartTime()) {
startDate
} else {
DateTime(startDate).withMillisOfDay(preferences.defaultDueTime).millis
}
if (task.reminderLast < startDateAlarm) {
return startDateAlarm
}
}
return NO_ALARM
}
/**
* Calculate the next alarm time for due date reminders.
*
*
* This alarm always returns the due date, and is triggered if the last reminder time occurred
* before the due date. This means it is possible to return due dates in the past.
*
*
* If the date was indicated to not have a due time, we read from preferences and assign a
* time.
*/
private fun calculateNextDueDateReminder(task: Task): Long {
if (task.hasDueDate() && task.isNotifyAtDeadline) {
val dueDate = task.dueDate
val dueDateAlarm = if (task.hasDueTime()) {
dueDate
} else {
DateTime(dueDate).withMillisOfDay(preferences.defaultDueTime).millis
}
if (task.reminderLast < dueDateAlarm) {
return dueDateAlarm
}
}
return NO_ALARM
}
/** /**
* Calculate the next alarm time for random reminders. * Calculate the next alarm time for random reminders.
* *

@ -16,6 +16,7 @@ import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.Recur 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.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
@ -116,7 +117,11 @@ class RepeatTaskHelper @Inject constructor(
} }
alarmService.getAlarms(taskId) alarmService.getAlarms(taskId)
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.onEach { it.time += newDueDate - oldDueDate } ?.onEach {
if (it.type == Alarm.TYPE_DATE_TIME) {
it.time += newDueDate - oldDueDate
}
}
?.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) } ?.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
} }

@ -17,12 +17,26 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.utility.TitleParser.parse 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.* import org.tasks.data.Alarm
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.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.Place
import org.tasks.data.Tag
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfDay
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class TaskCreator @Inject constructor( class TaskCreator @Inject constructor(
@ -34,7 +48,9 @@ class TaskCreator @Inject constructor(
private val googleTaskDao: GoogleTaskDao, private val googleTaskDao: GoogleTaskDao,
private val defaultFilterProvider: DefaultFilterProvider, private val defaultFilterProvider: DefaultFilterProvider,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val locationDao: LocationDao) { private val locationDao: LocationDao,
private val alarmDao: AlarmDao,
) {
suspend fun basicQuickAddTask(title: String): Task { suspend fun basicQuickAddTask(title: String): Task {
val task = createWithValues(title.trim { it <= ' ' }) val task = createWithValues(title.trim { it <= ' ' })
@ -71,6 +87,7 @@ class TaskCreator @Inject constructor(
} }
} }
taskDao.save(task, null) taskDao.save(task, null)
alarmDao.insert(task.getDefaultAlarms())
return task return task
} }
@ -161,10 +178,25 @@ class TaskCreator @Inject constructor(
private fun setDefaultReminders(preferences: Preferences, task: Task) { private fun setDefaultReminders(preferences: Preferences, task: Task) {
task.reminderPeriod = (DateUtilities.ONE_HOUR task.reminderPeriod = (DateUtilities.ONE_HOUR
* preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, 0)) * preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, 0))
task.reminderFlags = preferences.defaultReminders or preferences.defaultRingMode task.defaultReminders(preferences.defaultReminders)
task.ringFlags = preferences.defaultRingMode
} }
private fun Any?.substitute(): String? = private fun Any?.substitute(): String? =
(this as? String)?.let { PermaSql.replacePlaceholdersForNewTask(it) } (this as? String)?.let { PermaSql.replacePlaceholdersForNewTask(it) }
fun Task.getDefaultAlarms(): List<Alarm> = ArrayList<Alarm>().apply {
if (hasStartDate() && isNotifyAtStart) {
add(whenStarted(id))
}
if (hasDueDate()) {
if (isNotifyAtDeadline) {
add(whenDue(id))
}
if (isNotifyAfterDeadline) {
add(whenOverdue(id))
}
}
}
} }
} }

@ -16,13 +16,27 @@ import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.iCalendar.Companion.fromVtodo
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.data.* import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.CaldavTaskContainer
import org.tasks.data.FilterDao
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.Location
import org.tasks.data.LocationDao
import org.tasks.data.Tag
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.data.TaskAttachmentDao
import org.tasks.data.UpgraderDao
import org.tasks.data.UserActivityDao
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.widget.AppWidgetManager import org.tasks.widget.AppWidgetManager
import org.tasks.widget.WidgetPreferences import org.tasks.widget.WidgetPreferences
import java.io.File import java.io.File
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class Upgrader @Inject constructor( class Upgrader @Inject constructor(
@ -322,7 +336,7 @@ class Upgrader @Inject constructor(
private const val V5_3_0 = 491 private const val V5_3_0 = 491
private const val V6_0_beta_1 = 522 private const val V6_0_beta_1 = 522
private const val V6_0_beta_2 = 523 private const val V6_0_beta_2 = 523
private const val V6_4 = 546 const val V6_4 = 546
private const val V6_7 = 585 private const val V6_7 = 585
private const val V6_8_1 = 607 private const val V6_8_1 = 607
private const val V6_9 = 608 private const val V6_9 = 608
@ -337,6 +351,7 @@ class Upgrader @Inject constructor(
const val V9_7_3 = 90704 const val V9_7_3 = 90704
const val V10_0_2 = 100012 const val V10_0_2 = 100012
const val V11_13 = 111300 const val V11_13 = 111300
const val V12_3 = 120300
@JvmStatic @JvmStatic
fun getAndroidColor(context: Context, index: Int): Int { fun getAndroidColor(context: Context, index: Int): Int {

@ -15,18 +15,23 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity import org.tasks.activities.DateAndTimePickerActivity
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_REL_START
import org.tasks.data.Alarm.Companion.whenDue
import org.tasks.data.Alarm.Companion.whenOverdue
import org.tasks.data.Alarm.Companion.whenStarted
import org.tasks.databinding.ControlSetRemindersBinding import org.tasks.databinding.ControlSetRemindersBinding
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.locale.Locale import org.tasks.reminders.AlarmToString
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -38,9 +43,9 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ReminderControlSet : TaskEditControlFragment() { class ReminderControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var locale: Locale
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var alarmToString: AlarmToString
private lateinit var alertContainer: LinearLayout private lateinit var alertContainer: LinearLayout
private lateinit var mode: TextView private lateinit var mode: TextView
@ -53,15 +58,6 @@ class ReminderControlSet : TaskEditControlFragment() {
viewModel.ringFiveTimes!! -> setRingMode(1) viewModel.ringFiveTimes!! -> setRingMode(1)
else -> setRingMode(0) else -> setRingMode(0)
} }
if (viewModel.whenStart!!) {
addStart()
}
if (viewModel.whenDue!!) {
addDue()
}
if (viewModel.whenOverdue!!) {
addOverdue()
}
if (viewModel.reminderPeriod!! > 0) { if (viewModel.reminderPeriod!! > 0) {
addRandomReminder(viewModel.reminderPeriod!!) addRandomReminder(viewModel.reminderPeriod!!)
} }
@ -101,11 +97,16 @@ class ReminderControlSet : TaskEditControlFragment() {
private fun addAlarm(selected: String) { private fun addAlarm(selected: String) {
when (selected) { when (selected) {
getString(R.string.when_started) -> addStart() getString(R.string.when_started) ->
getString(R.string.when_due) -> addDue() addAlarmRow(whenStarted(viewModel.task?.id ?: 0))
getString(R.string.when_overdue) -> addOverdue() getString(R.string.when_due) ->
getString(R.string.randomly) -> addRandomReminder(TimeUnit.DAYS.toMillis(14)) addAlarmRow(whenDue(viewModel.task?.id ?: 0))
getString(R.string.pick_a_date_and_time) -> addNewAlarm() getString(R.string.when_overdue) ->
addAlarmRow(whenOverdue(viewModel.task?.id ?: 0))
getString(R.string.randomly) ->
addRandomReminder(TimeUnit.DAYS.toMillis(14))
getString(R.string.pick_a_date_and_time) ->
addNewAlarm()
} }
} }
@ -142,7 +143,7 @@ class ReminderControlSet : TaskEditControlFragment() {
if (requestCode == REQUEST_NEW_ALARM) { if (requestCode == REQUEST_NEW_ALARM) {
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 { 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)
viewModel.selectedAlarms?.add(alarm) viewModel.selectedAlarms?.add(alarm)
addAlarmRow(alarm) addAlarmRow(alarm)
@ -154,8 +155,13 @@ class ReminderControlSet : TaskEditControlFragment() {
} }
private fun addAlarmRow(alarm: Alarm) { private fun addAlarmRow(alarm: Alarm) {
addAlarmRow(DateUtilities.getLongDateStringWithTime(alarm.time, locale.locale)) { addAlarmRow(alarm) {
viewModel.selectedAlarms?.removeIf { it.time == alarm.time } viewModel.selectedAlarms?.removeIf {
it.type == alarm.type &&
it.time == alarm.time &&
it.repeat == alarm.repeat &&
it.interval == alarm.interval
}
} }
} }
@ -166,16 +172,17 @@ class ReminderControlSet : TaskEditControlFragment() {
startActivityForResult(intent, REQUEST_NEW_ALARM) startActivityForResult(intent, REQUEST_NEW_ALARM)
} }
private fun addAlarmRow(text: String, onRemove: View.OnClickListener): View { private fun addAlarmRow(alarm: Alarm, onRemove: View.OnClickListener): View {
val alertItem = requireActivity().layoutInflater.inflate(R.layout.alarm_edit_row, null) val alertItem = requireActivity().layoutInflater.inflate(R.layout.alarm_edit_row, null)
alertContainer.addView(alertItem) alertContainer.addView(alertItem)
addAlarmRow(alertItem, text, onRemove) addAlarmRow(alertItem, alarm, onRemove)
return alertItem return alertItem
} }
private fun addAlarmRow(alertItem: View, text: String, onRemove: View.OnClickListener?) { private fun addAlarmRow(alertItem: View, alarm: Alarm, onRemove: View.OnClickListener?) {
val display = alertItem.findViewById<TextView>(R.id.alarm_string) val display = alertItem.findViewById<TextView>(R.id.alarm_string)
display.text = text viewModel.selectedAlarms?.add(alarm)
display.text = alarmToString.toString(alarm)
alertItem alertItem
.findViewById<View>(R.id.clear) .findViewById<View>(R.id.clear)
.setOnClickListener { v: View? -> .setOnClickListener { v: View? ->
@ -187,13 +194,13 @@ class ReminderControlSet : TaskEditControlFragment() {
private val options: List<String> private val options: List<String>
get() { get() {
val options: MutableList<String> = ArrayList() val options: MutableList<String> = ArrayList()
if (viewModel.whenStart != true) { if (viewModel.selectedAlarms?.find { it.type == TYPE_REL_START && it.time == 0L } == null) {
options.add(getString(R.string.when_started)) options.add(getString(R.string.when_started))
} }
if (viewModel.whenDue != true) { if (viewModel.selectedAlarms?.find { it.type == TYPE_REL_END && it.time == 0L } == null) {
options.add(getString(R.string.when_due)) options.add(getString(R.string.when_due))
} }
if (viewModel.whenOverdue != true) { if (viewModel.selectedAlarms?.find { it.type == TYPE_REL_END && it.time == TimeUnit.HOURS.toMillis(24) } == null) {
options.add(getString(R.string.when_overdue)) options.add(getString(R.string.when_overdue))
} }
if (randomControlSet == null) { if (randomControlSet == null) {
@ -203,29 +210,8 @@ class ReminderControlSet : TaskEditControlFragment() {
return options return options
} }
private fun addStart() {
viewModel.whenStart = true
addAlarmRow(getString(R.string.when_started)) {
viewModel.whenStart = false
}
}
private fun addDue() {
viewModel.whenDue = true
addAlarmRow(getString(R.string.when_due)) {
viewModel.whenDue = false
}
}
private fun addOverdue() {
viewModel.whenOverdue = true
addAlarmRow(getString(R.string.when_overdue)) {
viewModel.whenOverdue = false
}
}
private fun addRandomReminder(reminderPeriod: Long) { private fun addRandomReminder(reminderPeriod: Long) {
val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ") { val alarmRow = addAlarmRow(Alarm(viewModel.task?.id ?: 0, 0, TYPE_RANDOM)) {
viewModel.reminderPeriod = 0 viewModel.reminderPeriod = 0
randomControlSet = null randomControlSet = null
} }

@ -7,13 +7,30 @@ import android.os.Handler
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.service.Upgrader import com.todoroo.astrid.service.Upgrader
import com.todoroo.astrid.service.Upgrader.Companion.V12_3
import com.todoroo.astrid.service.Upgrader.Companion.V6_4
import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.* import org.tasks.data.AlarmDao
import org.tasks.data.CaldavDao
import org.tasks.data.FilterDao
import org.tasks.data.Geofence
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.LocationDao
import org.tasks.data.Place.Companion.newPlace import org.tasks.data.Place.Companion.newPlace
import org.tasks.data.Tag
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.data.TaskAttachmentDao
import org.tasks.data.TaskListMetadataDao
import org.tasks.data.UserActivityDao
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import timber.log.Timber import timber.log.Timber
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -136,9 +153,19 @@ class TasksJsonImporter @Inject constructor(
alarm.task = taskId alarm.task = taskId
alarmDao.insert(alarm) alarmDao.insert(alarm)
} }
if (version < V12_3) {
task.defaultReminders(task.ringFlags)
alarmDao.insert(task.getDefaultAlarms())
task.ringFlags = when {
task.isNotifyModeFive -> Task.NOTIFY_MODE_FIVE
task.isNotifyModeNonstop -> Task.NOTIFY_MODE_NONSTOP
else -> 0
}
taskDao.save(task)
}
for (comment in backup.comments) { for (comment in backup.comments) {
comment.targetId = taskUuid comment.targetId = taskUuid
if (version < 546) { if (version < V6_4) {
comment.convertPictureUri() comment.convertPictureUri()
} }
userActivityDao.createNew(comment) userActivityDao.createNew(comment)
@ -177,7 +204,7 @@ class TasksJsonImporter @Inject constructor(
} }
backup.attachments?.forEach { attachment -> backup.attachments?.forEach { attachment ->
attachment.taskId = taskUuid attachment.taskId = taskUuid
if (version < 546) { if (version < V6_4) {
attachment.convertPathUri() attachment.convertPathUri()
} }
taskAttachmentDao.insert(attachment) taskAttachmentDao.insert(attachment)

@ -6,6 +6,8 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.tasks.time.DateTimeUtils.printTimestamp
import java.util.concurrent.TimeUnit
@Entity(tableName = "alarms") @Entity(tableName = "alarms")
class Alarm : Parcelable { class Alarm : Parcelable {
@ -21,6 +23,15 @@ class Alarm : Parcelable {
@ColumnInfo(name = "time") @ColumnInfo(name = "time")
var time: Long = 0 var time: Long = 0
@ColumnInfo(name = "type", defaultValue = "0")
var type: Int = 0
@ColumnInfo(name = "repeat", defaultValue = "0")
var repeat: Int = 0
@ColumnInfo(name = "interval", defaultValue = "0")
var interval: Long = 0
constructor() constructor()
@Ignore @Ignore
@ -28,25 +39,47 @@ class Alarm : Parcelable {
id = parcel.readLong() id = parcel.readLong()
task = parcel.readLong() task = parcel.readLong()
time = parcel.readLong() time = parcel.readLong()
type = parcel.readInt()
repeat = parcel.readInt()
interval = parcel.readLong()
} }
@Ignore @Ignore
constructor(task: Long, time: Long) { constructor(task: Long, time: Long, type: Int = 0, repeat: Int = 0, interval: Long = 0) {
this.task = task this.task = task
this.time = time this.time = time
this.type = type
this.repeat = repeat
this.interval = interval
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeLong(task)
parcel.writeLong(time)
parcel.writeInt(type)
parcel.writeInt(repeat)
parcel.writeLong(interval)
} }
override fun describeContents() = 0
override fun toString(): String { override fun toString(): String {
return "Alarm(id=$id, task=$task, time=$time)" return "Alarm(id=$id, task=$task, time=${printTimestamp(time)}, type=$type, repeat=$repeat, interval=$interval)"
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Alarm) return false if (javaClass != other?.javaClass) return false
other as Alarm
if (id != other.id) return false if (id != other.id) return false
if (task != other.task) return false if (task != other.task) return false
if (time != other.time) return false if (time != other.time) return false
if (type != other.type) return false
if (repeat != other.repeat) return false
if (interval != other.interval) return false
return true return true
} }
@ -55,20 +88,30 @@ class Alarm : Parcelable {
var result = id.hashCode() var result = id.hashCode()
result = 31 * result + task.hashCode() result = 31 * result + task.hashCode()
result = 31 * result + time.hashCode() result = 31 * result + time.hashCode()
result = 31 * result + type
result = 31 * result + repeat
result = 31 * result + interval.hashCode()
return result return result
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { companion object {
parcel.writeLong(id) const val TYPE_DATE_TIME = 0
parcel.writeLong(task) const val TYPE_REL_START = 1
parcel.writeLong(time) const val TYPE_REL_END = 2
} const val TYPE_RANDOM = 3
override fun describeContents() = 0 fun whenStarted(task: Long) = Alarm(task, 0, TYPE_REL_START)
fun whenDue(task: Long) = Alarm(task, 0, TYPE_REL_END)
fun whenOverdue(task: Long) =
Alarm(task, TimeUnit.DAYS.toMillis(1), TYPE_REL_END, 6, TimeUnit.DAYS.toMillis(1))
companion object CREATOR : Parcelable.Creator<Alarm> { @JvmField
override fun createFromParcel(parcel: Parcel) = Alarm(parcel) val CREATOR = object : Parcelable.Creator<Alarm> {
override fun createFromParcel(parcel: Parcel) = Alarm(parcel)
override fun newArray(size: Int): Array<Alarm?> = arrayOfNulls(size) override fun newArray(size: Int): Array<Alarm?> = arrayOfNulls(size)
}
} }
} }

@ -8,17 +8,26 @@ import com.todoroo.astrid.data.Task
@Dao @Dao
interface AlarmDao { interface AlarmDao {
@Query("SELECT alarms.* FROM alarms INNER JOIN tasks ON tasks._id = alarms.task " @Query("""
+ "WHERE tasks.completed = 0 AND tasks.deleted = 0 AND tasks.lastNotified < alarms.time " SELECT alarms.*
+ "ORDER BY time ASC") FROM alarms
INNER JOIN tasks ON tasks._id = alarms.task
WHERE tasks.completed = 0
AND tasks.deleted = 0
""")
suspend fun getActiveAlarms(): List<Alarm> suspend fun getActiveAlarms(): List<Alarm>
@Query("SELECT alarms.* FROM alarms INNER JOIN tasks ON tasks._id = alarms.task " @Query("""
+ "WHERE tasks._id = :taskId AND tasks.completed = 0 AND tasks.deleted = 0 AND tasks.lastNotified < alarms.time " SELECT alarms.*
+ "ORDER BY time ASC") FROM alarms
INNER JOIN tasks ON tasks._id = alarms.task
WHERE tasks._id = :taskId
AND tasks.completed = 0
AND tasks.deleted = 0
""")
suspend fun getActiveAlarms(taskId: Long): List<Alarm> suspend fun getActiveAlarms(taskId: Long): List<Alarm>
@Query("SELECT * FROM alarms WHERE task = :taskId ORDER BY time ASC") @Query("SELECT * FROM alarms WHERE task = :taskId")
suspend fun getAlarms(taskId: Long): List<Alarm> suspend fun getAlarms(taskId: Long): List<Alarm>
@Delete @Delete

@ -4,8 +4,14 @@ import android.database.sqlite.SQLiteException
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.todoroo.astrid.api.FilterListItem.NO_ORDER 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_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
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
object Migrations { object Migrations {
private val MIGRATION_35_36: Migration = object : Migration(35, 36) { private val MIGRATION_35_36: Migration = object : Migration(35, 36) {
@ -404,6 +410,26 @@ object Migrations {
} }
} }
private val MIGRATION_80_81 = object : Migration(80, 81) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `alarms` ADD COLUMN `type` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `alarms` ADD COLUMN `repeat` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `alarms` ADD COLUMN `interval` INTEGER NOT NULL DEFAULT 0")
database.execSQL(
"INSERT INTO `alarms` (`task`, `time`, `type`) SELECT `_id`, 0, $TYPE_REL_START FROM `tasks` WHERE `hideUntil` > 0 AND `notificationFlags` | $NOTIFY_AT_START"
)
database.execSQL(
"INSERT INTO `alarms` (`task`, `time`, `type`) SELECT `_id`, 0, $TYPE_REL_END FROM `tasks` WHERE `dueDate` > 0 AND `notificationFlags` | $NOTIFY_AT_DEADLINE"
)
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(
"UPDATE `tasks` SET `notificationFlags` = `notificationFlags` & ~$NOTIFY_AT_START & ~$NOTIFY_AT_DEADLINE & ~$NOTIFY_AFTER_DEADLINE"
)
}
}
val MIGRATIONS = arrayOf( val MIGRATIONS = arrayOf(
MIGRATION_35_36, MIGRATION_35_36,
MIGRATION_36_37, MIGRATION_36_37,
@ -441,6 +467,7 @@ object Migrations {
MIGRATION_77_78, MIGRATION_77_78,
MIGRATION_78_79, MIGRATION_78_79,
MIGRATION_79_80, MIGRATION_79_80,
MIGRATION_80_81,
) )
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

@ -1,8 +1,10 @@
package org.tasks.jobs; 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 com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.notifications.Notification; import org.tasks.notifications.Notification;
public class AlarmEntry implements NotificationQueueEntry { public class AlarmEntry implements NotificationQueueEntry {
@ -11,10 +13,6 @@ public class AlarmEntry implements NotificationQueueEntry {
private final long taskId; private final long taskId;
private final long time; private final long time;
public AlarmEntry(org.tasks.data.Alarm alarm) {
this(alarm.getId(), alarm.getTask(), alarm.getTime());
}
public AlarmEntry(long alarmId, long taskId, Long time) { public AlarmEntry(long alarmId, long taskId, Long time) {
this.alarmId = alarmId; this.alarmId = alarmId;
this.taskId = taskId; this.taskId = taskId;
@ -70,6 +68,6 @@ public class AlarmEntry implements NotificationQueueEntry {
@Override @Override
public String toString() { public String toString() {
return "AlarmEntry{" + "alarmId=" + alarmId + ", taskId=" + taskId + ", time=" + time + '}'; return "AlarmEntry{" + "alarmId=" + alarmId + ", taskId=" + taskId + ", time=" + printTimestamp(time) + '}';
} }
} }

@ -11,8 +11,14 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class NotificationQueue @Inject constructor(private val preferences: Preferences, private val workManager: WorkManager) { class NotificationQueue @Inject constructor(
private val jobs = TreeMultimap.create(Ordering.natural<Long>(), Comparator { l: NotificationQueueEntry, r: NotificationQueueEntry -> Ints.compare(l.hashCode(), r.hashCode()) }) private val preferences: Preferences,
private val workManager: WorkManager
) {
private val jobs =
TreeMultimap.create<Long, NotificationQueueEntry>(Ordering.natural()) { l, r ->
Ints.compare(l.hashCode(), r.hashCode())
}
@Synchronized @Synchronized
fun <T : NotificationQueueEntry> add(entry: T) = add(listOf(entry)) fun <T : NotificationQueueEntry> add(entry: T) = add(listOf(entry))

@ -1,6 +1,7 @@
package org.tasks.jobs; 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 org.tasks.notifications.Notification; import org.tasks.notifications.Notification;
@ -65,6 +66,6 @@ public class ReminderEntry implements NotificationQueueEntry {
@Override @Override
public String toString() { public String toString() {
return "ReminderEntry{" + "taskId=" + taskId + ", time=" + time + ", type=" + type + '}'; return "ReminderEntry{" + "taskId=" + taskId + ", time=" + printTimestamp(time) + ", type=" + type + '}';
} }
} }

@ -0,0 +1,76 @@
package org.tasks.reminders
import android.content.Context
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.data.Alarm
import org.tasks.locale.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.absoluteValue
class AlarmToString @Inject constructor(
@ApplicationContext context: Context,
var locale: Locale,
) {
private val resources = context.resources
fun toString(alarm: Alarm): String {
val reminder = when (alarm.type) {
Alarm.TYPE_REL_START ->
if (alarm.time == 0L) {
resources.getString(R.string.when_started)
} else {
val res = if (alarm.time < 0) {
R.string.alarm_before_start
} else {
R.string.alarm_after_start
}
resources.getString(res, getDurationString(alarm.time))
}
Alarm.TYPE_REL_END ->
if (alarm.time == 0L) {
resources.getString(R.string.when_due)
} else {
val res = if (alarm.time < 0) {
R.string.alarm_before_due
} else {
R.string.alarm_after_due
}
resources.getString(res, getDurationString(alarm.time))
}
Alarm.TYPE_RANDOM ->
resources.getString(R.string.randomly_once) + " "
else ->
DateUtilities.getLongDateStringWithTime(alarm.time, locale.locale)
}
return if (alarm.repeat > 0) {
val frequencyPlural = getDurationString(alarm.interval)
val count = alarm.repeat
val countString = resources.getQuantityString(R.plurals.repeat_times, count)
reminder + "\n" + resources.getString(R.string.repeats_plural_number_of_times, frequencyPlural, count, countString)
} else {
reminder
}
}
private fun getDurationString(duration: Long): String {
val seconds = duration.absoluteValue
val day = TimeUnit.MILLISECONDS.toDays(seconds)
val hours = TimeUnit.MILLISECONDS.toHours(seconds) - day * 24
val minute =
TimeUnit.MILLISECONDS.toMinutes(seconds) - TimeUnit.MILLISECONDS.toHours(seconds) * 60
val result = ArrayList<String>()
if (day > 0) {
result.add(resources.getQuantityString(R.plurals.repeat_n_days, day.toInt(), day.toInt()))
}
if (hours > 0) {
result.add(resources.getQuantityString(R.plurals.repeat_n_hours, hours.toInt(), hours.toInt()))
}
if (minute > 0) {
result.add(resources.getQuantityString(R.plurals.repeat_n_minutes, minute.toInt(), minute.toInt()))
}
return result.joinToString(" ")
}
}

@ -24,6 +24,7 @@ object DateTimeUtils {
MILLIS_PROVIDER = SYSTEM_MILLIS_PROVIDER MILLIS_PROVIDER = SYSTEM_MILLIS_PROVIDER
} }
@JvmStatic
fun printTimestamp(timestamp: Long): String = fun printTimestamp(timestamp: Long): String =
if (BuildConfig.DEBUG) Date(timestamp).toString() else timestamp.toString() if (BuildConfig.DEBUG) Date(timestamp).toString() else timestamp.toString()
@ -42,4 +43,7 @@ object DateTimeUtils {
fun Long.millisOfDay(): Int = if (this > 0) toDateTime().millisOfDay else 0 fun Long.millisOfDay(): Int = if (this > 0) toDateTime().millisOfDay else 0
fun Long.toDate(): net.fortuna.ical4j.model.Date? = this.toDateTime().toDate() fun Long.toDate(): net.fortuna.ical4j.model.Date? = this.toDateTime().toDate()
fun Long.withMillisOfDay(millisOfDay: Int): Long =
if (this > 0) toDateTime().withMillisOfDay(millisOfDay).millis else 0
} }

@ -35,6 +35,11 @@ import org.tasks.R
import org.tasks.Strings import org.tasks.Strings
import org.tasks.calendars.CalendarEventProvider import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
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.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask import org.tasks.data.CaldavTask
import org.tasks.data.GoogleTask import org.tasks.data.GoogleTask
@ -74,7 +79,8 @@ class TaskEditViewModel @Inject constructor(
private val googleTaskDao: GoogleTaskDao, private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val taskCompleter: TaskCompleter, private val taskCompleter: TaskCompleter,
private val alarmService: AlarmService) : ViewModel() { private val alarmService: AlarmService
) : ViewModel() {
val cleared = MutableLiveData<Event<Boolean>>() val cleared = MutableLiveData<Event<Boolean>>()
@ -90,7 +96,22 @@ class TaskEditViewModel @Inject constructor(
originalList = list originalList = list
originalLocation = location originalLocation = location
originalTags = tags.toImmutableList() originalTags = tags.toImmutableList()
originalAlarms = alarms.toList().toImmutableSet() originalAlarms =
if (isNew) {
ArrayList<Alarm>().apply {
if (task.isNotifyAtStart) {
add(whenStarted(0))
}
if (task.isNotifyAtDeadline) {
add(whenDue(0))
}
if (task.isNotifyAfterDeadline) {
add(whenOverdue(0))
}
}
} else {
alarms
}.toImmutableSet()
if (isNew && permissionChecker.canAccessCalendars()) { if (isNew && permissionChecker.canAccessCalendars()) {
originalCalendar = preferences.defaultCalendar originalCalendar = preferences.defaultCalendar
} }
@ -250,17 +271,8 @@ class TaskEditViewModel @Inject constructor(
var selectedAlarms: HashSet<Alarm>? = null var selectedAlarms: HashSet<Alarm>? = null
var whenStart: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_AT_START) ?: 0 > 0)
var whenDue: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_AT_DEADLINE) ?: 0 > 0)
var whenOverdue: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_AFTER_DEADLINE) ?: 0 > 0)
var ringNonstop: Boolean? = null var ringNonstop: Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_MODE_NONSTOP) ?: 0 > 0) get() = field ?: (task?.ringFlags?.and(Task.NOTIFY_MODE_NONSTOP) ?: 0 > 0)
set(value) { set(value) {
field = value field = value
if (value == true) { if (value == true) {
@ -269,7 +281,7 @@ class TaskEditViewModel @Inject constructor(
} }
var ringFiveTimes:Boolean? = null var ringFiveTimes:Boolean? = null
get() = field ?: (task?.reminderFlags?.and(Task.NOTIFY_MODE_FIVE) ?: 0 > 0) get() = field ?: (task?.ringFlags?.and(Task.NOTIFY_MODE_FIVE) ?: 0 > 0)
set(value) { set(value) {
field = value field = value
if (value == true) { if (value == true) {
@ -304,7 +316,7 @@ class TaskEditViewModel @Inject constructor(
originalTags?.toHashSet() != selectedTags?.toHashSet() || originalTags?.toHashSet() != selectedTags?.toHashSet() ||
newSubtasks.isNotEmpty() || newSubtasks.isNotEmpty() ||
it.reminderPeriod != reminderPeriod || it.reminderPeriod != reminderPeriod ||
it.reminderFlags != getReminderFlags() || it.ringFlags != getRingFlags() ||
originalAlarms != selectedAlarms originalAlarms != selectedAlarms
} ?: false } ?: false
@ -329,7 +341,7 @@ class TaskEditViewModel @Inject constructor(
it.repeatUntil = repeatUntil!! it.repeatUntil = repeatUntil!!
it.elapsedSeconds = elapsedSeconds!! it.elapsedSeconds = elapsedSeconds!!
it.estimatedSeconds = estimatedSeconds!! it.estimatedSeconds = estimatedSeconds!!
it.reminderFlags = getReminderFlags() it.ringFlags = getRingFlags()
it.reminderPeriod = reminderPeriod!! it.reminderPeriod = reminderPeriod!!
applyCalendarChanges() applyCalendarChanges()
@ -398,13 +410,23 @@ class TaskEditViewModel @Inject constructor(
} }
} }
if (selectedAlarms != originalAlarms) { if (!it.hasStartDate()) {
alarmService.synchronizeAlarms(it.id, selectedAlarms!!) selectedAlarms?.removeIf { a -> a.type == TYPE_REL_START }
it.modificationDate = now() }
if (!it.hasDueDate()) {
selectedAlarms?.removeIf { a -> a.type == TYPE_REL_END }
} }
taskDao.save(it, null) taskDao.save(it, null)
if (
selectedAlarms != originalAlarms ||
(isNew && selectedAlarms?.isNotEmpty() == true)
) {
alarmService.synchronizeAlarms(it.id, selectedAlarms!!)
it.modificationDate = now()
}
if (it.isCompleted != completed!!) { if (it.isCompleted != completed!!) {
taskCompleter.setComplete(it, completed!!) taskCompleter.setComplete(it, completed!!)
} }
@ -431,17 +453,8 @@ class TaskEditViewModel @Inject constructor(
} }
} }
private fun getReminderFlags(): Int { private fun getRingFlags(): Int {
var value = 0 var value = 0
if (whenStart == true) {
value = value or Task.NOTIFY_AT_START
}
if (whenDue == true) {
value = value or Task.NOTIFY_AT_DEADLINE
}
if (whenOverdue == true) {
value = value or Task.NOTIFY_AFTER_DEADLINE
}
value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv() value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv()
if (ringNonstop == true) { if (ringNonstop == true) {
value = value or Task.NOTIFY_MODE_NONSTOP value = value or Task.NOTIFY_MODE_NONSTOP

@ -716,4 +716,8 @@ File %1$s contained %2$s.\n\n
<string name="completed_tasks_at_bottom">Move completed tasks to bottom</string> <string name="completed_tasks_at_bottom">Move completed tasks to bottom</string>
<string name="completed_tasks_sort">Sort by completion date</string> <string name="completed_tasks_sort">Sort by completion date</string>
<string name="snackbar_tasks_completed">%d tasks completed</string> <string name="snackbar_tasks_completed">%d tasks completed</string>
<string name="alarm_before_start">%s before start</string>
<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>
</resources> </resources>

Loading…
Cancel
Save