diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d135f7a0..d57803123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### Next release + +* Sync snooze time with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync + * Compatible with Thunderbird +* Update translations + * Indonesian - when we were sober + ### 11.4 (2021-02-09) * Sync collapsed subtask state with Tasks.org, DAVx⁵, CalDAV, EteSync, and diff --git a/app/src/androidTest/java/org/tasks/opentasks/OpenTasksPropertiesTests.kt b/app/src/androidTest/java/org/tasks/opentasks/OpenTasksPropertiesTests.kt index 3a0850e29..cd7e39f3f 100644 --- a/app/src/androidTest/java/org/tasks/opentasks/OpenTasksPropertiesTests.kt +++ b/app/src/androidTest/java/org/tasks/opentasks/OpenTasksPropertiesTests.kt @@ -7,9 +7,12 @@ import dagger.hilt.android.testing.UninstallModules import kotlinx.coroutines.runBlocking import org.junit.Assert.* import org.junit.Test +import org.tasks.SuspendFreeze.Companion.freezeAt +import org.tasks.TestUtilities.withTZ 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.TagDao import org.tasks.data.TagDataDao import org.tasks.injection.ProductionModule @@ -25,7 +28,10 @@ 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.* import javax.inject.Inject @UninstallModules(ProductionModule::class) @@ -193,12 +199,89 @@ class OpenTasksPropertiesTests : OpenTasksTest() { ) } + @Test + fun readSnoozeTime() = runBlocking { + val (_, list) = withVtodo(SNOOZED) + + withTZ(CHICAGO) { + synchronizer.sync() + } + + val task = caldavDao + .getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") + ?.let { taskDao.fetch(it.task) } + + assertEquals(1612972355000, task!!.reminderSnooze) + } + + @Test + fun pushSnoozeTime() = withTZ(CHICAGO) { + val (listId, list) = openTaskDao.insertList() + val taskId = taskDao.createNew(newTask( + with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30)) + )) + + caldavDao.insert(newCaldavTask( + with(CALENDAR, list.uuid), + with(CaldavTaskMaker.TASK, taskId), + with(REMOTE_ID, "abcd") + )) + + freezeAt(DateTime(2021, 2, 4, 12, 30, 45, 125)) { + synchronizer.sync() + } + + assertEquals(1612467000000, openTaskDao.getTask(listId, "abcd")?.task!!.snooze) + } + + @Test + fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) { + val (listId, list) = openTaskDao.insertList() + val taskId = taskDao.createNew(newTask( + with(SNOOZE_TIME, DateTime(2021, 2, 4, 13, 30)) + )) + + caldavDao.insert(newCaldavTask( + with(CALENDAR, list.uuid), + with(CaldavTaskMaker.TASK, taskId), + with(REMOTE_ID, "abcd") + )) + + freezeAt(DateTime(2021, 2, 4, 13, 30, 45, 125)) { + synchronizer.sync() + } + + assertNull(openTaskDao.getTask(listId, "abcd")?.task!!.snooze) + } + + @Test + fun removeSnoozeTime() = runBlocking { + val (listId, list) = withVtodo(SNOOZED) + + synchronizer.sync() + + val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") + + taskDao.snooze(listOf(task!!.task), 0L) + + synchronizer.sync() + + assertNull( + openTaskDao + .getTask(listId, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") + ?.task + !!.snooze + ) + } + private suspend fun insertTag(task: Task, name: String) = newTagData(with(NAME, name)) .apply { tagDataDao.createNew(this) } .let { tagDao.insert(newTag(with(TASK, task), with(TAGDATA, it))) } companion object { + private val CHICAGO = TimeZone.getTimeZone("America/Chicago") + private val SUBTASK = """ BEGIN:VCALENDAR VERSION:2.0 @@ -260,5 +343,25 @@ class OpenTasksPropertiesTests : OpenTasksTest() { END:VTODO END:VCALENDAR """.trimIndent() + + private val SNOOZED = """ + BEGIN:VCALENDAR + PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN + VERSION:2.0 + BEGIN:VTODO + CREATED:20210210T151826Z + LAST-MODIFIED:20210210T152235Z + DTSTAMP:20210210T152235Z + UID:4CBBC669-70E3-474D-A0A3-0FC42A14A5A5 + SUMMARY:Test snooze + STATUS:NEEDS-ACTION + X-MOZ-LASTACK:20210210T152235Z + DTSTART;TZID=America/Chicago:20210210T091900 + DUE;TZID=America/Chicago:20210210T091900 + X-MOZ-SNOOZE-TIME:20210210T155235Z + X-MOZ-GENERATION:1 + END:VTODO + END:VCALENDAR + """.trimIndent() } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt index 5cb5e6c5f..de8e3d3cd 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -5,6 +5,7 @@ */ package com.todoroo.astrid.dao +import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.astrid.api.Filter import com.todoroo.astrid.data.Task import com.todoroo.astrid.reminders.ReminderService @@ -16,7 +17,7 @@ import org.tasks.LocalBroadcastManager import org.tasks.data.SubtaskInfo import org.tasks.data.TaskContainer import org.tasks.data.TaskDao -import org.tasks.date.DateTimeUtils.newDateTime +import org.tasks.date.DateTimeUtils.isAfterNow import org.tasks.db.SuspendDbUtils.eachChunk import org.tasks.jobs.WorkManager import org.tasks.location.GeofenceApi @@ -49,6 +50,11 @@ class TaskDao @Inject constructor( suspend fun setCompletionDate(remoteId: String, completionDate: Long) = taskDao.setCompletionDate(remoteId, completionDate) + suspend fun snooze(taskIds: List, snoozeTime: Long, updateTime: Long = now()) { + taskDao.snooze(taskIds, snoozeTime, updateTime) + syncAdapters.sync() + } + suspend fun getGoogleTasksToPush(account: String): List = taskDao.getGoogleTasksToPush(account) @@ -109,7 +115,10 @@ class TaskDao @Inject constructor( timerPlugin.stopTimer(task) } } - if (task.dueDate != original?.dueDate && newDateTime(task.dueDate).isAfterNow) { + if (task.reminderSnooze.isAfterNow()) { + notificationManager.cancel(task.id) + } + if (task.dueDate != original?.dueDate && task.dueDate.isAfterNow()) { notificationManager.cancel(task.id) } if (completionDateModified || deletionDateModified) { diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index 408ed7ecc..035ec45f3 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -317,6 +317,7 @@ class Task : Parcelable { && calendarURI == task.calendarURI && parent == task.parent && remoteId == task.remoteId + && reminderSnooze == task.reminderSnooze } fun googleTaskUpToDate(original: Task?): Boolean { @@ -350,6 +351,7 @@ class Task : Parcelable { && parent == original.parent && repeatUntil == original.repeatUntil && isCollapsed == original.isCollapsed + && reminderSnooze == original.reminderSnooze } val isSaved: Boolean diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index 6cfe31ad0..ef91a805c 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -23,6 +23,7 @@ import org.tasks.caldav.GeoUtils.toGeo import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.data.* import org.tasks.date.DateTimeUtils.newDateTime +import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.jobs.WorkManager import org.tasks.location.GeofenceApi import org.tasks.preferences.Preferences @@ -172,6 +173,8 @@ class iCalendar @Inject constructor( companion object { private const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER" private const val OC_HIDESUBTASKS = "X-OC-HIDESUBTASKS" + private const val MOZ_SNOOZE_TIME = "X-MOZ-SNOOZE-TIME" + private const val MOZ_LASTACK = "X-MOZ-LASTACK" private const val HIDE_SUBTASKS = "1" private val IS_PARENT = { r: RelatedTo -> r.parameters.getParameter(Parameter.RELTYPE).let { @@ -179,13 +182,10 @@ class iCalendar @Inject constructor( } } - private val IS_APPLE_SORT_ORDER = { x: Property? -> - x?.name.equals(APPLE_SORT_ORDER, true) - } - - private val IS_OC_HIDESUBTASKS = { x: Property? -> - x?.name.equals(OC_HIDESUBTASKS, true) - } + private val IS_APPLE_SORT_ORDER = { x: Property? -> x?.name.equals(APPLE_SORT_ORDER, true) } + private val IS_OC_HIDESUBTASKS = { x: Property? -> x?.name.equals(OC_HIDESUBTASKS, true) } + private val IS_MOZ_SNOOZE_TIME = { x: Property? -> x?.name.equals(MOZ_SNOOZE_TIME, true) } + private val IS_MOZ_LASTACK = { x: Property? -> x?.name.equals(MOZ_LASTACK, true) } fun Due?.apply(task: com.todoroo.astrid.data.Task) { task.dueDate = when (this?.date) { @@ -270,6 +270,32 @@ class iCalendar @Inject constructor( } } + var Task.snooze: Long? + get() = unknownProperties.find(IS_MOZ_SNOOZE_TIME)?.value?.let { + org.tasks.time.DateTime.from(DateTime(it)).toLocal().millis + } + set(value) { + value + ?.toDateTime() + ?.takeIf { it.isAfterNow } + ?.toUTC() + ?.let { DateTime(true).apply { time = it.millis } } + ?.let { utc -> + unknownProperties.find(IS_MOZ_SNOOZE_TIME) + ?.let { it.value = utc.toString() } + ?: unknownProperties.add( + XProperty(MOZ_SNOOZE_TIME, utc.toString()) + ) + val lastAck = DateTime(true).apply { time = lastModified!! } + unknownProperties.find(IS_MOZ_LASTACK) + ?.let { it.value = lastAck.toString() } + ?: unknownProperties.add( + XProperty(MOZ_LASTACK, lastAck.toString()) + ) + } + ?: unknownProperties.removeIf(IS_MOZ_SNOOZE_TIME) + } + fun com.todoroo.astrid.data.Task.applyRemote(remote: Task) { val completedAt = remote.completedAt if (completedAt != null) { @@ -297,6 +323,7 @@ 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) { @@ -358,6 +385,7 @@ class iCalendar @Inject constructor( parent = if (task.parent == 0L) null else caldavTask.remoteParent order = caldavTask.order collapsed = task.isCollapsed + snooze = task.reminderSnooze } private fun getDate(timestamp: Long): Date { diff --git a/app/src/main/java/org/tasks/data/TaskDao.kt b/app/src/main/java/org/tasks/data/TaskDao.kt index b38ca696d..58cf8b344 100644 --- a/app/src/main/java/org/tasks/data/TaskDao.kt +++ b/app/src/main/java/org/tasks/data/TaskDao.kt @@ -53,8 +53,8 @@ abstract class TaskDao(private val database: Database) { @Query("UPDATE tasks SET completed = :completionDate " + "WHERE remoteId = :remoteId") abstract suspend fun setCompletionDate(remoteId: String, completionDate: Long) - @Query("UPDATE tasks SET snoozeTime = :millis WHERE _id in (:taskIds)") - abstract suspend fun snooze(taskIds: List, millis: Long) + @Query("UPDATE tasks SET snoozeTime = :snoozeTime, modified = :updateTime WHERE _id in (:taskIds)") + internal abstract suspend fun snooze(taskIds: List, snoozeTime: Long, updateTime: Long = now()) @Query("SELECT tasks.* FROM tasks " + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " @@ -186,9 +186,6 @@ FROM recursive_tasks if (!task.insignificantChange(original)) { task.modificationDate = now() } - if (task.dueDate != original?.dueDate) { - task.reminderSnooze = 0 - } return updateInternal(task) == 1 } diff --git a/app/src/main/java/org/tasks/date/DateTimeUtils.kt b/app/src/main/java/org/tasks/date/DateTimeUtils.kt index 187213701..eea7cf4ba 100644 --- a/app/src/main/java/org/tasks/date/DateTimeUtils.kt +++ b/app/src/main/java/org/tasks/date/DateTimeUtils.kt @@ -27,4 +27,6 @@ object DateTimeUtils { fun Long.toAppleEpoch(): Long = DateTime(this).toAppleEpoch() fun Long.toDateTime(): DateTime = DateTime(this) + + fun Long.isAfterNow(): Boolean = DateTime(this).isAfterNow } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/reminders/SnoozeActivity.kt b/app/src/main/java/org/tasks/reminders/SnoozeActivity.kt index 9683d0cdd..b2be0676e 100644 --- a/app/src/main/java/org/tasks/reminders/SnoozeActivity.kt +++ b/app/src/main/java/org/tasks/reminders/SnoozeActivity.kt @@ -6,17 +6,15 @@ import android.content.DialogInterface import android.content.Intent import android.os.Bundle import androidx.lifecycle.lifecycleScope +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.data.TaskDao import org.tasks.dialogs.MyTimePickerDialog import org.tasks.injection.InjectingAppCompatActivity import org.tasks.notifications.NotificationManager -import org.tasks.reminders.SnoozeActivity.Companion.EXTRA_TASK_IDS -import org.tasks.reminders.SnoozeActivity.Companion.FLAGS import org.tasks.themes.ThemeAccent import org.tasks.time.DateTime import java.util.*