Skip to content

Commit

Permalink
Merge pull request #76 from opatry/fix-local-only-sync
Browse files Browse the repository at this point in the history
Fix local only tasks sync
  • Loading branch information
opatry authored Oct 28, 2024
2 parents de58ae2 + 0221c30 commit 6e0d758
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,25 @@ data class Task(
val link: String
)
}

/**
* Factory function to create a new [TaskList] exposing only relevant parameters.
*
* @property title Title of the task. Maximum length allowed: 1024 characters.
* @property notes Notes describing the task. Tasks assigned from Google Docs cannot have notes. Optional. Maximum length allowed: 8192 characters.
* @property status Status of the task. This is either [Status.NeedsAction] or [Status.Completed].
* @property dueDate Due date of the task (as a RFC 3339 timestamp). Optional. The due date only records date information; the time portion of the timestamp is discarded when setting the due date. It isn't possible to read or write the time that a task is due via the API.
* @property completedDate Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.
*/
fun Task(
title: String,
notes: String? = null,
status: Status = Status.NeedsAction,
dueDate: Instant? = null,
completedDate: Instant? = null
): Task {
require(title.length <= 1024) { "Title length must be at most 1024 characters" }
require(notes == null || notes.length <= 8192) { "Notes length must be at most 8192 characters" }
// need to artificially define an extra parameter to call data class ctor instead of recursive call
return Task(id = "", title = title, notes = notes, status = status, dueDate = dueDate, completedDate = completedDate)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,14 @@ data class TaskList(
@SerialName("selfLink")
val selfLink: String = "",
)

/**
* Factory function to create a new [TaskList] exposing only relevant parameters.
*
* @param title Title of the task list. Maximum length allowed: 1024 characters.
*/
fun TaskList(title: String): TaskList {
require(title.length <= 1024) { "Title length must be at most 1024 characters" }
// need to artificially define an extra parameter to call data class ctor instead of recursive call
return TaskList(id = "", title = title)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,20 @@ import net.opatry.tasks.data.entity.TaskEntity

@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: TaskEntity): Long

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(item: TaskEntity): Long

@Query("SELECT * FROM task WHERE remote_id = :remoteId")
suspend fun getByRemoteId(remoteId: String): TaskEntity?

@Query("SELECT * FROM task")
fun getAllAsFlow(): Flow<List<TaskEntity>>

@Query("SELECT * FROM task WHERE remote_id IS NULL")
suspend fun getLocalOnlyTasks(): List<TaskEntity>
@Query("SELECT * FROM task WHERE parent_list_local_id = :taskListLocalId AND remote_id IS NULL")
suspend fun getLocalOnlyTasks(taskListLocalId: Long): List<TaskEntity>

// FIXME should be a pending deletion "flag" until sync is done
@Query("DELETE FROM task WHERE local_id = :id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ import net.opatry.tasks.data.entity.TaskListEntity

@Dao
interface TaskListDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: TaskListEntity): Long

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(item: TaskListEntity): Long

// FIXME should be a pending deletion "flag" until sync is done
@Query("DELETE FROM task_list WHERE local_id = :id")
suspend fun deleteTaskList(id: Long)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ private fun TaskEntity.asTask(): Task {
updatedDate = lastUpdateDate,
status = if (isCompleted) Task.Status.Completed else Task.Status.NeedsAction,
completedDate = completionDate,
// doc says it's a read only field, but status is not hidden when syncing local only completed tasks
// forcing the hidden status works and makes everything more consistent (position following 099999... pattern, hidden status)
isHidden = isCompleted,
position = position,
)
}
Expand Down Expand Up @@ -206,38 +209,44 @@ class TaskRepository(
// - etc.
val existingEntity = taskListDao.getByRemoteId(remoteTaskList.id)
val updatedEntity = remoteTaskList.asTaskListEntity(existingEntity?.id, existingEntity?.sorting ?: TaskListEntity.Sorting.UserDefined)
val finalLocalId = taskListDao.insert(updatedEntity)
val finalLocalId = taskListDao.upsert(updatedEntity)
taskListIds[finalLocalId] = remoteTaskList.id
}
taskListDao.deleteStaleTaskLists(remoteTaskLists.map(TaskList::id))
taskListDao.getLocalOnlyTaskLists().onEach { localTaskList ->
val remoteId = try {
taskListsApi.insert(TaskList(title = localTaskList.title)).id
} catch (e: Exception) {
null
val remoteTaskList = withContext(Dispatchers.IO) {
try {
taskListsApi.insert(TaskList(localTaskList.title))
} catch (e: Exception) {
null
}
}
if (remoteId != null) {
taskListDao.insert(localTaskList.copy(remoteId = remoteId))
if (remoteTaskList != null) {
taskListDao.upsert(remoteTaskList.asTaskListEntity(localTaskList.id, localTaskList.sorting))
}
}
taskListIds.forEach { (localListId, remoteListId) ->
// TODO deal with showDeleted, showHidden, etc.
// TODO updatedMin could be used to filter out unchanged tasks since last sync
// /!\ this would impact the deleteStaleTasks logic
val remoteTasks = tasksApi.listAll(remoteListId, showHidden = true, showCompleted = true)
val remoteTasks = withContext(Dispatchers.IO) {
tasksApi.listAll(remoteListId, showHidden = true, showCompleted = true)
}
remoteTasks.onEach { remoteTask ->
val existingEntity = taskDao.getByRemoteId(remoteTask.id)
taskDao.insert(remoteTask.asTaskEntity(localListId, existingEntity?.id))
taskDao.upsert(remoteTask.asTaskEntity(localListId, existingEntity?.id))
}
taskDao.deleteStaleTasks(localListId, remoteTasks.map(Task::id))
taskDao.getLocalOnlyTasks().onEach { localTask ->
val remoteId = try {
tasksApi.insert(remoteListId, Task(title = localTask.title)).id
} catch (e: Exception) {
null
taskDao.getLocalOnlyTasks(localListId).onEach { localTask ->
val remoteTask = withContext(Dispatchers.IO) {
try {
tasksApi.insert(remoteListId, localTask.asTask())
} catch (e: Exception) {
null
}
}
if (remoteId != null) {
taskDao.insert(localTask.copy(remoteId = remoteId))
if (remoteTask != null) {
taskDao.upsert(remoteTask.asTaskEntity(localListId, localTask.id))
}
}
}
Expand All @@ -254,7 +263,7 @@ class TaskRepository(
}
}
if (taskList != null) {
taskListDao.insert(taskList.asTaskListEntity(taskListId, TaskListEntity.Sorting.UserDefined))
taskListDao.upsert(taskList.asTaskListEntity(taskListId, TaskListEntity.Sorting.UserDefined))
}
}

Expand All @@ -280,7 +289,7 @@ class TaskRepository(
title = newTitle,
lastUpdateDate = now
)
taskListDao.insert(taskListEntity)
taskListDao.upsert(taskListEntity)
if (taskListEntity.remoteId != null) {
withContext(Dispatchers.IO) {
try {
Expand Down Expand Up @@ -345,16 +354,13 @@ class TaskRepository(
if (taskListEntity.remoteId != null) {
val task = withContext(Dispatchers.IO) {
try {
tasksApi.insert(
taskListEntity.remoteId,
taskEntity.asTask()
)
tasksApi.insert(taskListEntity.remoteId, taskEntity.asTask())
} catch (e: Exception) {
null
}
}
if (task != null) {
taskDao.insert(task.asTaskEntity(taskListId, taskId))
taskDao.upsert(task.asTaskEntity(taskListId, taskId))
}
}
}
Expand Down Expand Up @@ -386,7 +392,7 @@ class TaskRepository(
val task = requireNotNull(taskDao.getById(taskId)) { "Invalid task id $taskId" }
val updatedTaskEntity = updateLogic(task, now) ?: return

taskDao.insert(updatedTaskEntity)
taskDao.upsert(updatedTaskEntity)

// FIXME should already be available in entity, quick & dirty workaround
val taskListRemoteId = updatedTaskEntity.parentTaskRemoteId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ import net.opatry.tasks.data.entity.UserEntity

@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(user: UserEntity): Long

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(user: UserEntity): Long

@Query("SELECT * FROM user WHERE remote_id = :remoteId")
suspend fun getByRemoteId(remoteId: String): UserEntity?

Expand All @@ -61,6 +64,6 @@ interface UserDao {
@Transaction
suspend fun setSignedInUser(userEntity: UserEntity) {
clearAllSignedInStatus()
insert(userEntity.copy(isSignedIn = true))
upsert(userEntity.copy(isSignedIn = true))
}
}

0 comments on commit 6e0d758

Please sign in to comment.