Synchronize snooze time

pull/1369/head
Alex Baker 5 years ago
parent bfd1316c33
commit 211f56a343

@ -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) ### 11.4 (2021-02-09)
* Sync collapsed subtask state with Tasks.org, DAVx⁵, CalDAV, EteSync, and * Sync collapsed subtask state with Tasks.org, DAVx⁵, CalDAV, EteSync, and

@ -7,9 +7,12 @@ import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test 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.collapsed
import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.caldav.iCalendar.Companion.snooze
import org.tasks.data.TagDao import org.tasks.data.TagDao
import org.tasks.data.TagDataDao import org.tasks.data.TagDataDao
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@ -25,7 +28,10 @@ import org.tasks.makers.TagMaker.TASK
import org.tasks.makers.TagMaker.newTag import org.tasks.makers.TagMaker.newTag
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.COLLAPSED import org.tasks.makers.TaskMaker.COLLAPSED
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @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) = private suspend fun insertTag(task: Task, name: String) =
newTagData(with(NAME, name)) newTagData(with(NAME, name))
.apply { tagDataDao.createNew(this) } .apply { tagDataDao.createNew(this) }
.let { tagDao.insert(newTag(with(TASK, task), with(TAGDATA, it))) } .let { tagDao.insert(newTag(with(TASK, task), with(TAGDATA, it))) }
companion object { companion object {
private val CHICAGO = TimeZone.getTimeZone("America/Chicago")
private val SUBTASK = """ private val SUBTASK = """
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
@ -260,5 +343,25 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
END:VTODO END:VTODO
END:VCALENDAR END:VCALENDAR
""".trimIndent() """.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()
} }
} }

@ -5,6 +5,7 @@
*/ */
package com.todoroo.astrid.dao package com.todoroo.astrid.dao
import com.todoroo.andlib.utility.DateUtilities.now
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
@ -16,7 +17,7 @@ import org.tasks.LocalBroadcastManager
import org.tasks.data.SubtaskInfo import org.tasks.data.SubtaskInfo
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao 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.db.SuspendDbUtils.eachChunk
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
@ -49,6 +50,11 @@ class TaskDao @Inject constructor(
suspend fun setCompletionDate(remoteId: String, completionDate: Long) = suspend fun setCompletionDate(remoteId: String, completionDate: Long) =
taskDao.setCompletionDate(remoteId, completionDate) taskDao.setCompletionDate(remoteId, completionDate)
suspend fun snooze(taskIds: List<Long>, snoozeTime: Long, updateTime: Long = now()) {
taskDao.snooze(taskIds, snoozeTime, updateTime)
syncAdapters.sync()
}
suspend fun getGoogleTasksToPush(account: String): List<Task> = suspend fun getGoogleTasksToPush(account: String): List<Task> =
taskDao.getGoogleTasksToPush(account) taskDao.getGoogleTasksToPush(account)
@ -109,7 +115,10 @@ class TaskDao @Inject constructor(
timerPlugin.stopTimer(task) 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) notificationManager.cancel(task.id)
} }
if (completionDateModified || deletionDateModified) { if (completionDateModified || deletionDateModified) {

@ -317,6 +317,7 @@ class Task : Parcelable {
&& calendarURI == task.calendarURI && calendarURI == task.calendarURI
&& parent == task.parent && parent == task.parent
&& remoteId == task.remoteId && remoteId == task.remoteId
&& reminderSnooze == task.reminderSnooze
} }
fun googleTaskUpToDate(original: Task?): Boolean { fun googleTaskUpToDate(original: Task?): Boolean {
@ -350,6 +351,7 @@ class Task : Parcelable {
&& parent == original.parent && parent == original.parent
&& repeatUntil == original.repeatUntil && repeatUntil == original.repeatUntil
&& isCollapsed == original.isCollapsed && isCollapsed == original.isCollapsed
&& reminderSnooze == original.reminderSnooze
} }
val isSaved: Boolean val isSaved: Boolean

@ -23,6 +23,7 @@ import org.tasks.caldav.GeoUtils.toGeo
import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.* import org.tasks.data.*
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -172,6 +173,8 @@ class iCalendar @Inject constructor(
companion object { companion object {
private const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER" private const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER"
private const val OC_HIDESUBTASKS = "X-OC-HIDESUBTASKS" 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 const val HIDE_SUBTASKS = "1"
private val IS_PARENT = { r: RelatedTo -> private val IS_PARENT = { r: RelatedTo ->
r.parameters.getParameter<RelType>(Parameter.RELTYPE).let { r.parameters.getParameter<RelType>(Parameter.RELTYPE).let {
@ -179,13 +182,10 @@ class iCalendar @Inject constructor(
} }
} }
private val IS_APPLE_SORT_ORDER = { x: Property? -> private val IS_APPLE_SORT_ORDER = { x: Property? -> x?.name.equals(APPLE_SORT_ORDER, true) }
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) }
private val IS_OC_HIDESUBTASKS = { x: Property? ->
x?.name.equals(OC_HIDESUBTASKS, true)
}
fun Due?.apply(task: com.todoroo.astrid.data.Task) { fun Due?.apply(task: com.todoroo.astrid.data.Task) {
task.dueDate = when (this?.date) { 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) { fun com.todoroo.astrid.data.Task.applyRemote(remote: Task) {
val completedAt = remote.completedAt val completedAt = remote.completedAt
if (completedAt != null) { if (completedAt != null) {
@ -297,6 +323,7 @@ class iCalendar @Inject constructor(
remote.due.apply(this) remote.due.apply(this)
remote.dtStart.apply(this) remote.dtStart.apply(this)
isCollapsed = remote.collapsed isCollapsed = remote.collapsed
reminderSnooze = remote.snooze ?: 0
} }
fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) { fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) {
@ -358,6 +385,7 @@ class iCalendar @Inject constructor(
parent = if (task.parent == 0L) null else caldavTask.remoteParent parent = if (task.parent == 0L) null else caldavTask.remoteParent
order = caldavTask.order order = caldavTask.order
collapsed = task.isCollapsed collapsed = task.isCollapsed
snooze = task.reminderSnooze
} }
private fun getDate(timestamp: Long): Date { private fun getDate(timestamp: Long): Date {

@ -53,8 +53,8 @@ abstract class TaskDao(private val database: Database) {
@Query("UPDATE tasks SET completed = :completionDate " + "WHERE remoteId = :remoteId") @Query("UPDATE tasks SET completed = :completionDate " + "WHERE remoteId = :remoteId")
abstract suspend fun setCompletionDate(remoteId: String, completionDate: Long) abstract suspend fun setCompletionDate(remoteId: String, completionDate: Long)
@Query("UPDATE tasks SET snoozeTime = :millis WHERE _id in (:taskIds)") @Query("UPDATE tasks SET snoozeTime = :snoozeTime, modified = :updateTime WHERE _id in (:taskIds)")
abstract suspend fun snooze(taskIds: List<Long>, millis: Long) internal abstract suspend fun snooze(taskIds: List<Long>, snoozeTime: Long, updateTime: Long = now())
@Query("SELECT tasks.* FROM tasks " @Query("SELECT tasks.* FROM tasks "
+ "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task "
@ -186,9 +186,6 @@ FROM recursive_tasks
if (!task.insignificantChange(original)) { if (!task.insignificantChange(original)) {
task.modificationDate = now() task.modificationDate = now()
} }
if (task.dueDate != original?.dueDate) {
task.reminderSnooze = 0
}
return updateInternal(task) == 1 return updateInternal(task) == 1
} }

@ -27,4 +27,6 @@ object DateTimeUtils {
fun Long.toAppleEpoch(): Long = DateTime(this).toAppleEpoch() fun Long.toAppleEpoch(): Long = DateTime(this).toAppleEpoch()
fun Long.toDateTime(): DateTime = DateTime(this) fun Long.toDateTime(): DateTime = DateTime(this)
fun Long.isAfterNow(): Boolean = DateTime(this).isAfterNow
} }

@ -6,17 +6,15 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.reminders.ReminderService import com.todoroo.astrid.reminders.ReminderService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.activities.DateAndTimePickerActivity import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.data.TaskDao
import org.tasks.dialogs.MyTimePickerDialog import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.notifications.NotificationManager import org.tasks.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.themes.ThemeAccent
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.* import java.util.*

Loading…
Cancel
Save