Sync subtask expand/collapse state

pull/1360/head
Alex Baker 5 years ago
parent 99dee06c64
commit e4e37b22f1

@ -6,7 +6,9 @@ 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.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.getParent import org.tasks.caldav.iCalendar.Companion.getParent
import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.data.TagDao import org.tasks.data.TagDao
@ -23,6 +25,7 @@ import org.tasks.makers.TagMaker.TAGDATA
import org.tasks.makers.TagMaker.TASK 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.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@ -143,6 +146,34 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
) )
} }
@Test
fun readCollapsedState() = runBlocking {
val (_, list) = withVtodo(HIDE_SUBTASKS)
synchronizer.sync()
val task = caldavDao
.getTaskByRemoteId(list.uuid!!, "2822976a-b71e-4962-92e4-db7297789c20")
?.let { taskDao.fetch(it.task) }
assertTrue(task!!.isCollapsed)
}
@Test
fun pushCollapsedState() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(with(COLLAPSED, true)))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(CaldavTaskMaker.TASK, taskId),
with(REMOTE_ID, "abcd")
))
synchronizer.sync()
assertTrue(openTaskDao.getTask(listId, "abcd")?.task!!.collapsed)
}
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) }
@ -195,5 +226,20 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
END:VTODO END:VTODO
END:VCALENDAR END:VCALENDAR
""".trimIndent() """.trimIndent()
private val HIDE_SUBTASKS = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO
UID:2822976a-b71e-4962-92e4-db7297789c20
CREATED:20210209T104536
LAST-MODIFIED:20210209T104548
DTSTAMP:20210209T104548
SUMMARY:Parent
X-OC-HIDESUBTASKS:1
END:VTODO
END:VCALENDAR
""".trimIndent()
} }
} }

@ -31,6 +31,7 @@ object TaskMaker {
val PRIORITY: Property<Task, Int> = newProperty() val PRIORITY: Property<Task, Int> = newProperty()
val PARENT: Property<Task, Long> = newProperty() val PARENT: Property<Task, Long> = newProperty()
val UUID: Property<Task, String> = newProperty() val UUID: Property<Task, String> = newProperty()
val COLLAPSED: Property<Task, Boolean> = newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Task> -> private val instantiator = Instantiator { lookup: PropertyLookup<Task> ->
val task = Task() val task = Task()
@ -85,6 +86,7 @@ object TaskMaker {
lookup.valueOf(RECUR, null as String?)?.let { lookup.valueOf(RECUR, null as String?)?.let {
task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false)) task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false))
} }
task.isCollapsed = lookup.valueOf(COLLAPSED, false)
task.uuid = lookup.valueOf(UUID, NO_UUID) task.uuid = lookup.valueOf(UUID, NO_UUID)
val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime()) val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime())
task.creationDate = creationTime.millis task.creationDate = creationTime.millis

@ -71,10 +71,15 @@ class TaskDao @Inject constructor(
suspend fun getChildren(id: Long): List<Long> = taskDao.getChildren(id) suspend fun getChildren(id: Long): List<Long> = taskDao.getChildren(id)
suspend fun setCollapsed(id: Long, collapsed: Boolean) = taskDao.setCollapsed(id, collapsed) suspend fun setCollapsed(id: Long, collapsed: Boolean) {
taskDao.setCollapsed(listOf(id), collapsed)
syncAdapters.sync()
}
suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) = suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
taskDao.setCollapsed(preferences, filter, collapsed) taskDao.setCollapsed(preferences, filter, collapsed)
syncAdapters.sync()
}
// --- save // --- save
// TODO: get rid of this super-hack // TODO: get rid of this super-hack

@ -349,6 +349,7 @@ class Task : Parcelable {
&& recurrence == original.recurrence && recurrence == original.recurrence
&& parent == original.parent && parent == original.parent
&& repeatUntil == original.repeatUntil && repeatUntil == original.repeatUntil
&& isCollapsed == original.isCollapsed
} }
val isSaved: Boolean val isSaved: Boolean

@ -119,7 +119,6 @@ class iCalendar @Inject constructor(
suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task, remoteModel: Task) { suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task, remoteModel: Task) {
remoteModel.applyLocal(caldavTask, task) remoteModel.applyLocal(caldavTask, task)
remoteModel.order = caldavTask.order
val categories = remoteModel.categories val categories = remoteModel.categories
categories.clear() categories.clear()
categories.addAll(tagDataDao.getTagDataForTask(task.id).map { it.name!! }) categories.addAll(tagDataDao.getTagDataForTask(task.id).map { it.name!! })
@ -172,6 +171,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 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 {
it === RelType.PARENT || it == null || it.value.isNullOrBlank() it === RelType.PARENT || it == null || it.value.isNullOrBlank()
@ -182,6 +183,10 @@ class iCalendar @Inject constructor(
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)
}
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) {
null -> 0 null -> 0
@ -244,21 +249,28 @@ class iCalendar @Inject constructor(
} }
var Task.order: Long? var Task.order: Long?
get() = unknownProperties get() = unknownProperties.find(IS_APPLE_SORT_ORDER).let { it?.value?.toLongOrNull() }
.find { it.name?.equals(APPLE_SORT_ORDER, true) == true }
.let { it?.value?.toLongOrNull() }
set(order) { set(order) {
if (order == null) { if (order == null) {
unknownProperties.removeAll(unknownProperties.filter(IS_APPLE_SORT_ORDER)) unknownProperties.removeIf(IS_APPLE_SORT_ORDER)
} else { } else {
val existingOrder = unknownProperties unknownProperties
.find { it.name?.equals(APPLE_SORT_ORDER, true) == true } .find(IS_APPLE_SORT_ORDER)
?.let { it.value = order.toString() }
?: unknownProperties.add(XProperty(APPLE_SORT_ORDER, order.toString()))
}
}
if (existingOrder != null) { var Task.collapsed: Boolean
existingOrder.value = order.toString() get() = unknownProperties.find(IS_OC_HIDESUBTASKS).let { it?.value == HIDE_SUBTASKS }
} else { set(collapsed) {
unknownProperties.add(XProperty(APPLE_SORT_ORDER, order.toString())) if (collapsed) {
} unknownProperties
.find(IS_OC_HIDESUBTASKS)
?.let { it.value = HIDE_SUBTASKS }
?: unknownProperties.add(XProperty(OC_HIDESUBTASKS, HIDE_SUBTASKS))
} else {
unknownProperties.removeIf(IS_OC_HIDESUBTASKS)
} }
} }
@ -288,6 +300,7 @@ class iCalendar @Inject constructor(
setRecurrence(remote.rRule?.recur) setRecurrence(remote.rRule?.recur)
remote.due.apply(this) remote.due.apply(this)
remote.dtStart.apply(this) remote.dtStart.apply(this)
isCollapsed = remote.collapsed
} }
fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) { fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) {
@ -347,6 +360,8 @@ class iCalendar @Inject constructor(
else -> if (priority > 5) min(9, priority) else 9 else -> if (priority > 5) min(9, priority) else 9
} }
setParent(if (task.parent == 0L) null else caldavTask.remoteParent) setParent(if (task.parent == 0L) null else caldavTask.remoteParent)
order = caldavTask.order
collapsed = task.isCollapsed
} }
private fun getDate(timestamp: Long): Date { private fun getDate(timestamp: Long): Date {

@ -6,7 +6,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import com.todoroo.andlib.sql.Criterion import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Field import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.sql.Functions import com.todoroo.andlib.sql.Functions
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.PermaSql import com.todoroo.astrid.api.PermaSql
import com.todoroo.astrid.dao.Database import com.todoroo.astrid.dao.Database
@ -24,7 +24,7 @@ import timber.log.Timber
abstract class TaskDao(private val database: Database) { abstract class TaskDao(private val database: Database) {
@Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0 AND (hideUntil > :now OR dueDate > :now)") @Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0 AND (hideUntil > :now OR dueDate > :now)")
internal abstract suspend fun needsRefresh(now: Long = DateUtilities.now()): List<Task> internal abstract suspend fun needsRefresh(now: Long = now()): List<Task>
@Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1") @Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1")
abstract suspend fun fetch(id: Long): Task? abstract suspend fun fetch(id: Long): Task?
@ -101,14 +101,14 @@ abstract class TaskDao(private val database: Database) {
open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List<String>, subtasks: SubtaskInfo): List<TaskContainer> = open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List<String>, subtasks: SubtaskInfo): List<TaskContainer> =
database.withTransaction { database.withTransaction {
val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 val start = if (BuildConfig.DEBUG) now() else 0
val queries = callback(subtasks) val queries = callback(subtasks)
val last = queries.size - 1 val last = queries.size - 1
for (i in 0 until last) { for (i in 0 until last) {
query(SimpleSQLiteQuery(queries[i])) query(SimpleSQLiteQuery(queries[i]))
} }
val result = fetchTasks(SimpleSQLiteQuery(queries[last])) val result = fetchTasks(SimpleSQLiteQuery(queries[last]))
Timber.v("%sms: %s", DateUtilities.now() - start, queries.joinToString(";\n")) Timber.v("%sms: %s", now() - start, queries.joinToString(";\n"))
result result
} }
@ -169,25 +169,22 @@ FROM recursive_tasks
""") """)
abstract suspend fun getChildren(ids: List<Long>): List<Long> abstract suspend fun getChildren(ids: List<Long>): List<Long>
@Query("UPDATE tasks SET collapsed = :collapsed WHERE _id = :id") internal suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
abstract suspend fun setCollapsed(id: Long, collapsed: Boolean)
open suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
fetchTasks(preferences, filter) fetchTasks(preferences, filter)
.filter(TaskContainer::hasChildren) .filter(TaskContainer::hasChildren)
.map(TaskContainer::getId) .map(TaskContainer::getId)
.eachChunk { collapse(it, collapsed) } .eachChunk { setCollapsed(it, collapsed) }
} }
@Query("UPDATE tasks SET collapsed = :collapsed WHERE _id IN (:ids)") @Query("UPDATE tasks SET collapsed = :collapsed, modified = :now WHERE _id IN (:ids)")
internal abstract suspend fun collapse(ids: List<Long>, collapsed: Boolean) internal abstract suspend fun setCollapsed(ids: List<Long>, collapsed: Boolean, now: Long = now())
@Insert @Insert
abstract suspend fun insert(task: Task): Long abstract suspend fun insert(task: Task): Long
suspend fun update(task: Task, original: Task? = null): Boolean { suspend fun update(task: Task, original: Task? = null): Boolean {
if (!task.insignificantChange(original)) { if (!task.insignificantChange(original)) {
task.modificationDate = DateUtilities.now() task.modificationDate = now()
} }
if (task.dueDate != original?.dueDate) { if (task.dueDate != original?.dueDate) {
task.reminderSnooze = 0 task.reminderSnooze = 0
@ -201,7 +198,7 @@ FROM recursive_tasks
suspend fun createNew(task: Task): Long { suspend fun createNew(task: Task): Long {
task.id = NO_ID task.id = NO_ID
if (task.creationDate == 0L) { if (task.creationDate == 0L) {
task.creationDate = DateUtilities.now() task.creationDate = now()
} }
if (Task.isUuidEmpty(task.remoteId)) { if (Task.isUuidEmpty(task.remoteId)) {
task.remoteId = UUIDHelper.newUUID() task.remoteId = UUIDHelper.newUUID()
@ -216,9 +213,9 @@ FROM recursive_tasks
suspend fun count(filter: Filter): Int { suspend fun count(filter: Filter): Int {
val query = getQuery(filter.sqlQuery, Field.COUNT) val query = getQuery(filter.sqlQuery, Field.COUNT)
val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 val start = if (BuildConfig.DEBUG) now() else 0
val count = count(query) val count = count(query)
Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) Timber.v("%sms: %s", now() - start, query.sql)
return count return count
} }
@ -226,9 +223,9 @@ FROM recursive_tasks
suspend fun fetchFiltered(queryTemplate: String): List<Task> { suspend fun fetchFiltered(queryTemplate: String): List<Task> {
val query = getQuery(queryTemplate, Task.FIELDS) val query = getQuery(queryTemplate, Task.FIELDS)
val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 val start = if (BuildConfig.DEBUG) now() else 0
val tasks = fetchTasks(query) val tasks = fetchTasks(query)
Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) Timber.v("%sms: %s", now() - start, query.sql)
return tasks.map(TaskContainer::getTask) return tasks.map(TaskContainer::getTask)
} }

@ -29,6 +29,7 @@ import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.activity.MainActivity import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator

@ -2,6 +2,7 @@ package org.tasks.widget
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.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -9,7 +10,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.TaskDao
import org.tasks.dialogs.BaseDateTimePicker.OnDismissHandler import org.tasks.dialogs.BaseDateTimePicker.OnDismissHandler
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity

Loading…
Cancel
Save