Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import net.opatry.google.tasks.TasksApi
import net.opatry.google.tasks.model.ResourceListResponse
import net.opatry.google.tasks.model.ResourceType
import net.opatry.google.tasks.model.Task
import net.opatry.tasks.data.toTaskPosition
import java.net.ConnectException
import kotlin.concurrent.atomics.AtomicLong
import kotlin.concurrent.atomics.ExperimentalAtomicApi
Expand Down Expand Up @@ -56,6 +57,15 @@ class InMemoryTasksApi(
return logic()
}

private fun recomputeTaskPositions(tasks: List<Task>): List<Task> {
val nextPositions = mutableMapOf<String?, Int>(null to 0)
return tasks.map { task ->
val position = nextPositions.getOrDefault(task.parent, 0)
nextPositions[task.parent] = position + 1
task.copy(position = position.toTaskPosition())
}
}

override suspend fun clear(taskListId: String) {
handleRequest("clear") {
synchronized(this) {
Expand Down Expand Up @@ -89,7 +99,7 @@ class InMemoryTasksApi(
task
}
}
storage[taskListId] = tasks
storage[taskListId] = recomputeTaskPositions(tasks).toMutableList()
}
}
}
Expand All @@ -105,22 +115,25 @@ class InMemoryTasksApi(

override suspend fun insert(taskListId: String, task: Task, parentTaskId: String?, previousTaskId: String?): Task {
return handleRequest("insert") {
val previousTaskIndex = storage[taskListId]
?.indexOfFirst { it.id == previousTaskId }
?: -1
val newTask = task.copy(
id = taskId.addAndFetch(1).toString(),
etag = "etag",
title = task.title,
updatedDate = Clock.System.now(),
selfLink = "selfLink",
parent = parentTaskId,
position = "00000000000000000000", // TODO compute position from previous
position = "", // will be updated with all together by recomputeTaskPositions
)
synchronized(this) {
val tasks = storage.getOrDefault(taskListId, mutableListOf())
val previousTaskIndex = tasks.indexOfFirst { it.id == previousTaskId }.coerceAtLeast(0)
tasks.add(previousTaskIndex, newTask)
storage[taskListId] = tasks
tasks.add(previousTaskIndex + 1, newTask)
val positionedTasks = recomputeTaskPositions(tasks)
storage[taskListId] = positionedTasks.toMutableList()
positionedTasks[previousTaskIndex + 1]
}
newTask
}
}

Expand Down Expand Up @@ -216,18 +229,21 @@ class InMemoryTasksApi(
if (parentTaskId != null && destinationTasks.none { it.id == parentTaskId }) {
error("Task ($parentTaskId) not found in task list ($targetListId)")
}
val previousTaskIndex = destinationTasks.indexOfFirst { it.id == previousTaskId }
if (previousTaskIndex == -1) {
val pivotId = previousTaskId ?: parentTaskId
val previousTaskIndex = destinationTasks.indexOfFirst { it.id == pivotId }
if (previousTaskId != null && previousTaskIndex == -1) {
error("Task ($previousTaskId) not found in task list ($targetListId)")
}
// TODO "position"
val moved = task.copy(parent = parentTaskId)
destinationTasks.removeIf { it.id == moved.id }
destinationTasks.add(previousTaskIndex + 1, moved)
storage[targetListId] = destinationTasks
val positionedDestinationTasks = recomputeTaskPositions(destinationTasks)
storage[targetListId] = positionedDestinationTasks.toMutableList()
if (taskListId != targetListId) {
storage[taskListId] = tasks.filter { it.id != taskId }.toMutableList()
val sourceTasks = tasks.filter { it.id != taskId }
storage[taskListId] = recomputeTaskPositions(sourceTasks).toMutableList()
}
moved
positionedDestinationTasks[previousTaskIndex + 1]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import net.opatry.tasks.data.entity.TaskEntity

// this MUST not be a valid ID in the database (IDs should start at 0)
private const val NULL_ID_SENTINEL = -1L

@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
Expand Down Expand Up @@ -63,31 +60,23 @@ interface TaskDao {
@Query("DELETE FROM task WHERE local_id IN (:ids)")
suspend fun deleteTasks(ids: List<Long>)

// using COALESCE to handle nulls for convenience & simplicity
// it requires that no task ID is equal to `NULL_ID_SENTINEL` (which is the case)
// alternatively, we could use a OR condition and check both nulls or both equals
// `parent_local_id = :parentTaskLocalId OR (:parentTaskLocalId IS NULL AND parent_local_id IS NULL)`
@Query(
"""
SELECT * FROM task
WHERE parent_list_local_id = :taskListLocalId
AND COALESCE(parent_local_id, $NULL_ID_SENTINEL) = COALESCE(:parentTaskLocalId, $NULL_ID_SENTINEL)
AND ((parent_local_id IS NULL AND :parentTaskLocalId IS NULL) OR parent_local_id = :parentTaskLocalId)
AND position <= :position
AND is_completed = false
ORDER BY position ASC
"""
)
suspend fun getTasksUpToPosition(taskListLocalId: Long, parentTaskLocalId: Long?, position: String): List<TaskEntity>

// using COALESCE to handle nulls for convenience & simplicity
// it requires that no task ID is equal to `NULL_ID_SENTINEL` (which is the case)
// alternatively, we could use a OR condition and check both nulls or both equals
// `parent_local_id = :parentTaskLocalId OR (:parentTaskLocalId IS NULL AND parent_local_id IS NULL)`
@Query(
"""
SELECT * FROM task
WHERE parent_list_local_id = :taskListLocalId
AND COALESCE(parent_local_id, $NULL_ID_SENTINEL) = COALESCE(:parentTaskLocalId, $NULL_ID_SENTINEL)
AND ((parent_local_id IS NULL AND :parentTaskLocalId IS NULL) OR parent_local_id = :parentTaskLocalId)
AND position >= :position
AND is_completed = false
ORDER BY position ASC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private fun TaskList.asTaskListEntity(localId: Long?, sorting: TaskListEntity.So
)
}

private fun Task.asTaskEntity(parentLocalId: Long, localId: Long?, parentTaskLocalId: Long? = null): TaskEntity {
private fun Task.asTaskEntity(parentLocalId: Long, localId: Long?, parentTaskLocalId: Long?): TaskEntity {
return TaskEntity(
id = localId ?: 0,
remoteId = id,
Expand Down Expand Up @@ -281,7 +281,8 @@ class TaskRepository(
}
remoteTasks.onEach { remoteTask ->
val existingEntity = taskDao.getByRemoteId(remoteTask.id)
taskDao.upsert(remoteTask.asTaskEntity(localListId, existingEntity?.id))
val parentTaskEntity = remoteTask.parent?.let { taskDao.getByRemoteId(it) }
taskDao.upsert(remoteTask.asTaskEntity(localListId, existingEntity?.id, parentTaskEntity?.id))
}
taskDao.deleteStaleTasks(localListId, remoteTasks.map(Task::id))
taskDao.getLocalOnlyTasks(localListId).onEach { localTask ->
Expand All @@ -293,7 +294,8 @@ class TaskRepository(
}
}
if (remoteTask != null) {
taskDao.upsert(remoteTask.asTaskEntity(localListId, localTask.id))
val parentTaskEntity = remoteTask.parent?.let { taskDao.getByRemoteId(it) }
taskDao.upsert(remoteTask.asTaskEntity(localListId, localTask.id, parentTaskEntity?.id))
}
}
}
Expand Down Expand Up @@ -483,7 +485,7 @@ class TaskRepository(
}

if (task != null) {
taskDao.upsert(task.asTaskEntity(updatedTaskEntity.parentListLocalId, taskId))
taskDao.upsert(task.asTaskEntity(updatedTaskEntity.parentListLocalId, taskId, updatedTaskEntity.parentTaskLocalId))
}
}
}
Expand Down Expand Up @@ -582,7 +584,7 @@ class TaskRepository(
}

if (task != null) {
taskDao.upsert(task.asTaskEntity(updatedTaskEntity.parentListLocalId, taskId))
taskDao.upsert(task.asTaskEntity(updatedTaskEntity.parentListLocalId, taskId, null))
}
}
}
Expand Down Expand Up @@ -635,7 +637,7 @@ class TaskRepository(
}

if (task != null) {
taskDao.upsert(task.asTaskEntity(destinationListId, taskId))
taskDao.upsert(task.asTaskEntity(destinationListId, taskId, null))
}
}
}
Expand Down