diff options
Diffstat (limited to 'android/app/src/main/java/org')
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt | 56 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/work/AddTaskWorker.kt | 122 |
2 files changed, 142 insertions, 36 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt index 5d658e6..acd3447 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.glance.appwidget.updateAll import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -27,6 +26,7 @@ import org.terst.doot.widget.data.Project import org.terst.doot.widget.data.TaskRecurrence import org.terst.doot.widget.data.WidgetRepository import org.terst.doot.widget.data.dataStore +import org.terst.doot.widget.work.AddTaskWorker class QuickAddActivity : ComponentActivity() { @@ -79,44 +79,28 @@ class QuickAddActivity : ComponentActivity() { }, onAdd = { title, description -> lifecycleScope.launch { - val r = repo() - if (r == null) { + // A local DataStore read, not a network call -- fast enough to + // check before dismissing, so a missing config doesn't silently + // eat what was typed. + val prefs = this@QuickAddActivity.dataStore.data.first() + val configured = !prefs[Keys.SERVER_URL].isNullOrBlank() && !prefs[Keys.TOKEN].isNullOrBlank() + if (!configured) { toast("Not configured: set server URL/token in Settings") return@launch } - r.addTask(title).onSuccess { id -> - // addTask succeeding only means the task exists -- each - // follow-up call below can independently fail (network - // blip, expired token) without aborting the others, so - // collect what didn't stick rather than assume success - // silently the way the pre-fix code did for addTask itself. - val failed = mutableListOf<String>() - if (description.isNotBlank()) { - r.updateTask(id, title, description).onFailure { failed += "description" } - } - dueDate?.let { if (r.reschedule(id, "doot", it).isFailure) failed += "due date" } - project?.let { if (r.setTaskProject(id, it.id).isFailure) failed += "project" } - if (labels.isNotEmpty()) { - // Same "assign a default color the first time it's colored" - // rule as the edit popup's label editor, so a quick-add task - // with new labels doesn't leave them silently uncolored. - val newlyAdded = labels.filter { it !in knownLabels } - newlyAdded.forEach { label -> r.setLabelColor(label, colorForNewLabel(label)) } - if (r.setTaskLabels(id, labels).isFailure) failed += "labels" - } - recurrence?.let { - if (r.setTaskRecurrence(id, it.freq, it.interval, it.weekdays).isFailure) failed += "recurrence" - } - - r.fetchAndPersist(this@QuickAddActivity) - DootWidget().updateAll(this@QuickAddActivity) - if (failed.isNotEmpty()) { - toast("Task added, but couldn't save: ${failed.joinToString(", ")}") - } - finish() - }.onFailure { e -> - toast("Couldn't add task: ${e.message ?: "network error"}") - } + // Dismiss and confirm immediately instead of blocking on addTask + // plus up to five sequential follow-up calls (description/due + // date/project/labels/recurrence) and a widget refresh -- that + // chain previously left the sheet sitting on screen doing + // nothing visible for a couple of seconds. AddTaskWorker runs + // the actual submission in the background, surviving this + // activity finishing (lifecycleScope wouldn't). + toast("Added \"$title\"") + AddTaskWorker.enqueue( + applicationContext, title, description, dueDate, + project?.id, labels, recurrence + ) + finish() } }, onDismiss = ::finish diff --git a/android/app/src/main/java/org/terst/doot/widget/work/AddTaskWorker.kt b/android/app/src/main/java/org/terst/doot/widget/work/AddTaskWorker.kt new file mode 100644 index 0000000..c038629 --- /dev/null +++ b/android/app/src/main/java/org/terst/doot/widget/work/AddTaskWorker.kt @@ -0,0 +1,122 @@ +package org.terst.doot.widget.work + +import android.content.Context +import android.widget.Toast +import androidx.glance.appwidget.updateAll +import androidx.work.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import org.terst.doot.widget.data.Keys +import org.terst.doot.widget.data.TaskRecurrence +import org.terst.doot.widget.data.WidgetRepository +import org.terst.doot.widget.data.dataStore +import org.terst.doot.widget.ui.DootWidget +import org.terst.doot.widget.ui.colorForNewLabel + +/** + * Runs Quick Add's actual submission -- addTask plus up to five follow-up + * calls (description/due date/project/labels/recurrence), a data refresh, + * and a widget update -- as background work that survives QuickAddActivity + * finishing immediately on tap, instead of the activity's own lifecycleScope + * (which the previous in-place implementation used, and which gets cancelled + * the moment the activity is destroyed). See QuickAddActivity.onAdd: the + * sheet now dismisses and confirms right away rather than sitting on screen + * through this whole sequential chain. + */ +class AddTaskWorker(context: Context, params: WorkerParameters) : + CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + val title = inputData.getString(KEY_TITLE) ?: return Result.failure() + val description = inputData.getString(KEY_DESCRIPTION).orEmpty() + val dueDate = inputData.getString(KEY_DUE_DATE) + val projectId = inputData.getString(KEY_PROJECT_ID) + val labels = inputData.getStringArray(KEY_LABELS)?.toList().orEmpty() + val recFreq = inputData.getString(KEY_REC_FREQ) + val recInterval = inputData.getInt(KEY_REC_INTERVAL, 1) + val recWeekdays = inputData.getIntArray(KEY_REC_WEEKDAYS)?.toList().orEmpty() + + val prefs = applicationContext.dataStore.data.first() + val url = prefs[Keys.SERVER_URL]?.takeIf { it.isNotBlank() } ?: return Result.failure() + val token = prefs[Keys.TOKEN]?.takeIf { it.isNotBlank() } ?: return Result.failure() + val repo = WidgetRepository(okhttp3.OkHttpClient(), url, token) + + val id = repo.addTask(title).getOrElse { e -> + toast("Couldn't add task: ${e.message ?: "network error"}") + return Result.failure() + } + + // Each follow-up can independently fail (network blip, expired token) + // without aborting the others -- collect what didn't stick rather + // than assume success silently. + val failed = mutableListOf<String>() + if (description.isNotBlank()) { + repo.updateTask(id, title, description).onFailure { failed += "description" } + } + dueDate?.let { if (repo.reschedule(id, "doot", it).isFailure) failed += "due date" } + projectId?.let { if (repo.setTaskProject(id, it).isFailure) failed += "project" } + if (labels.isNotEmpty()) { + // Same "assign a default color the first time it's colored" rule + // as the edit popup's label editor, so a quick-add task with new + // labels doesn't leave them silently uncolored. Fetches current + // known labels fresh here rather than trusting a snapshot passed + // in from the activity, since this may run well after the sheet + // closed. + val known = repo.fetchLabelColors().getOrNull()?.map { it.name }.orEmpty() + labels.filter { it !in known }.forEach { repo.setLabelColor(it, colorForNewLabel(it)) } + if (repo.setTaskLabels(id, labels).isFailure) failed += "labels" + } + if (!recFreq.isNullOrEmpty()) { + if (repo.setTaskRecurrence(id, recFreq, recInterval, recWeekdays).isFailure) failed += "recurrence" + } + + repo.fetchAndPersist(applicationContext) + DootWidget().updateAll(applicationContext) + + if (failed.isNotEmpty()) { + toast("Task added, but couldn't save: ${failed.joinToString(", ")}") + } + return Result.success() + } + + private suspend fun toast(message: String) = withContext(Dispatchers.Main) { + Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show() + } + + companion object { + private const val KEY_TITLE = "title" + private const val KEY_DESCRIPTION = "description" + private const val KEY_DUE_DATE = "due_date" + private const val KEY_PROJECT_ID = "project_id" + private const val KEY_LABELS = "labels" + private const val KEY_REC_FREQ = "rec_freq" + private const val KEY_REC_INTERVAL = "rec_interval" + private const val KEY_REC_WEEKDAYS = "rec_weekdays" + + fun enqueue( + context: Context, + title: String, + description: String, + dueDate: String?, + projectId: String?, + labels: List<String>, + recurrence: TaskRecurrence? + ) { + val data = workDataOf( + KEY_TITLE to title, + KEY_DESCRIPTION to description, + KEY_DUE_DATE to dueDate, + KEY_PROJECT_ID to projectId, + KEY_LABELS to labels.toTypedArray(), + KEY_REC_FREQ to recurrence?.freq, + KEY_REC_INTERVAL to (recurrence?.interval ?: 1), + KEY_REC_WEEKDAYS to (recurrence?.weekdays?.toIntArray() ?: IntArray(0)) + ) + val request = OneTimeWorkRequestBuilder<AddTaskWorker>() + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(request) + } + } +} |
