diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncBootstrap.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncBootstrap.kt
index 42e8e3b..c9472b0 100644
--- a/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncBootstrap.kt
+++ b/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncBootstrap.kt
@@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
+import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -61,7 +62,7 @@ class StorageSyncBootstrap @Inject constructor(
}
syncRunner.enqueue(
displayTitle = uiStrings(R.string.task_title_storage_sync_background),
- logReason = "debounce",
+ reason = StorageSyncTriggerReason.Debounce,
)
}
}
diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncWorker.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncWorker.kt
index f9c2ca3..1d2c930 100644
--- a/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncWorker.kt
+++ b/app/src/main/java/com/github/nullptroma/wallenc/app/sync/StorageSyncWorker.kt
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
+import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -19,7 +20,7 @@ class StorageSyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
Timber.d("Periodic storage sync started (attempt %d)", runAttemptCount)
return runCatching {
- syncRunner.runBlocking()
+ syncRunner.runBlocking(StorageSyncTriggerReason.Background)
Timber.d("Periodic storage sync finished")
Result.success()
}.getOrElse { error ->
diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/ITaskOrchestrator.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/ITaskOrchestrator.kt
index 4b204df..c706492 100644
--- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/ITaskOrchestrator.kt
+++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/ITaskOrchestrator.kt
@@ -20,4 +20,7 @@ interface ITaskOrchestrator {
fun cancel(taskId: TaskId): Boolean
fun cancelAll()
+
+ /** Запись в общий лог пайплайна вне контекста [TaskContext] (например, WorkManager sync). */
+ fun appendPipelineLog(level: TaskLogLevel, key: TaskLogKey)
}
diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/PipelineTask.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/PipelineTask.kt
index c7bb443..9c01538 100644
--- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/PipelineTask.kt
+++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/PipelineTask.kt
@@ -6,6 +6,7 @@ import java.util.UUID
data class PipelineTask(
val id: TaskId,
val title: String,
+ val enqueuedAtMs: Long,
val dispatcher: CoroutineDispatcher,
val state: TaskRunState,
/** UUID storage, для которого идёт задача (кнопки только этой строки в UI). */
diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/StorageSyncTriggerReason.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/StorageSyncTriggerReason.kt
new file mode 100644
index 0000000..09acaaf
--- /dev/null
+++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/StorageSyncTriggerReason.kt
@@ -0,0 +1,8 @@
+package com.github.nullptroma.wallenc.domain.tasks
+
+/** Источник запуска синхронизации хранилищ (для логов пайплайна задач). */
+enum class StorageSyncTriggerReason {
+ Debounce,
+ SyncTab,
+ Background,
+}
diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/TaskLogKey.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/TaskLogKey.kt
index 6341159..b2f0856 100644
--- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/TaskLogKey.kt
+++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/tasks/TaskLogKey.kt
@@ -3,7 +3,10 @@ package com.github.nullptroma.wallenc.domain.tasks
import com.github.nullptroma.wallenc.domain.errors.WallencException
sealed class TaskLogKey {
- data object SyncStarted : TaskLogKey()
- data object SyncFinished : TaskLogKey()
- data class SyncFailed(val error: WallencException) : TaskLogKey()
+ data class SyncStarted(val reason: StorageSyncTriggerReason) : TaskLogKey()
+ data class SyncFinished(val reason: StorageSyncTriggerReason) : TaskLogKey()
+ data class SyncFailed(
+ val error: WallencException,
+ val reason: StorageSyncTriggerReason,
+ ) : TaskLogKey()
}
diff --git a/task-runtime/src/main/java/com/github/nullptroma/wallenc/task/runtime/TaskOrchestrator.kt b/task-runtime/src/main/java/com/github/nullptroma/wallenc/task/runtime/TaskOrchestrator.kt
index 7b40820..f6016f5 100644
--- a/task-runtime/src/main/java/com/github/nullptroma/wallenc/task/runtime/TaskOrchestrator.kt
+++ b/task-runtime/src/main/java/com/github/nullptroma/wallenc/task/runtime/TaskOrchestrator.kt
@@ -73,6 +73,7 @@ class TaskOrchestrator(
val task = PipelineTask(
id = id,
title = title,
+ enqueuedAtMs = System.currentTimeMillis(),
dispatcher = dispatcher,
state = TaskRunState.Queued,
busyStorageUuid = busyStorageUuid,
@@ -104,6 +105,10 @@ class TaskOrchestrator(
}
}
+ override fun appendPipelineLog(level: TaskLogLevel, key: TaskLogKey) {
+ appendLogLine(level, message = "", logKey = key)
+ }
+
private fun replaceTask(id: TaskId, fn: (PipelineTask) -> PipelineTask) {
synchronized(tasksById) {
val cur = tasksById[id] ?: return
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskLogKeys.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskLogKeys.kt
index cb39529..54999d0 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskLogKeys.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskLogKeys.kt
@@ -1,13 +1,31 @@
package com.github.nullptroma.wallenc.ui.resources
+import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
import com.github.nullptroma.wallenc.ui.R
fun TaskLogKey.resolve(resolver: UiStringResolver): String = when (this) {
- TaskLogKey.SyncStarted -> resolver(R.string.task_log_sync_started)
- TaskLogKey.SyncFinished -> resolver(R.string.task_log_sync_finished)
+ is TaskLogKey.SyncStarted -> resolver(
+ R.string.task_log_sync_started,
+ resolver.resolveSyncTriggerReason(reason),
+ )
+ is TaskLogKey.SyncFinished -> resolver(
+ R.string.task_log_sync_finished,
+ resolver.resolveSyncTriggerReason(reason),
+ )
is TaskLogKey.SyncFailed -> {
val notification = error.toUserNotification().resolve(resolver)
- resolver(R.string.task_log_sync_failed, notification)
+ resolver(
+ R.string.task_log_sync_failed,
+ resolver.resolveSyncTriggerReason(reason),
+ notification,
+ )
}
}
+
+private fun UiStringResolver.resolveSyncTriggerReason(reason: StorageSyncTriggerReason): String =
+ when (reason) {
+ StorageSyncTriggerReason.Debounce -> this(R.string.task_sync_trigger_debounce)
+ StorageSyncTriggerReason.SyncTab -> this(R.string.task_sync_trigger_sync_tab)
+ StorageSyncTriggerReason.Background -> this(R.string.task_sync_trigger_background)
+ }
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskPipelineTimeFormat.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskPipelineTimeFormat.kt
new file mode 100644
index 0000000..67bac9e
--- /dev/null
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/resources/TaskPipelineTimeFormat.kt
@@ -0,0 +1,13 @@
+package com.github.nullptroma.wallenc.ui.resources
+
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+
+private val pipelineTimeFormatter: DateTimeFormatter =
+ DateTimeFormatter.ofPattern("HH:mm:ss")
+
+fun formatTaskPipelineTime(timestampMs: Long): String =
+ Instant.ofEpochMilli(timestampMs)
+ .atZone(ZoneId.systemDefault())
+ .format(pipelineTimeFormatter)
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/tasks/TaskPipelineScreen.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/tasks/TaskPipelineScreen.kt
index 920be22..ecf742a 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/tasks/TaskPipelineScreen.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/tasks/TaskPipelineScreen.kt
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
@@ -36,7 +37,9 @@ import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
+import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
import com.github.nullptroma.wallenc.ui.resources.displayText
+import com.github.nullptroma.wallenc.ui.resources.formatTaskPipelineTime
import com.github.nullptroma.wallenc.ui.resources.resolveText
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
@@ -91,18 +94,7 @@ fun TaskPipelineScreen(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(logs.size) { i ->
- val line = logs[i]
- val prefix = when (line.level) {
- TaskLogLevel.Debug -> "D"
- TaskLogLevel.Info -> "I"
- TaskLogLevel.Warn -> "W"
- TaskLogLevel.Error -> "E"
- }
- Text(
- "[$prefix] ${line.displayText()}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ PipelineLogRow(line = logs[i])
}
}
Button(
@@ -178,14 +170,59 @@ fun TaskPipelineScreen(
}
}
+@Composable
+private fun PipelineLogRow(line: TaskLogLine) {
+ val prefix = when (line.level) {
+ TaskLogLevel.Debug -> "D"
+ TaskLogLevel.Info -> "I"
+ TaskLogLevel.Warn -> "W"
+ TaskLogLevel.Error -> "E"
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top,
+ ) {
+ Text(
+ text = "[$prefix] ${line.displayText()}",
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = formatTaskPipelineTime(line.timestampMs),
+ modifier = Modifier.widthIn(min = 56.dp),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+
@Composable
private fun TaskRow(task: PipelineTask, isRunning: Boolean) {
Column(Modifier.fillMaxWidth()) {
- Text(
- task.title,
- style = if (isRunning) MaterialTheme.typography.titleSmall
- else MaterialTheme.typography.bodyMedium,
- )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = task.title,
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp),
+ style = if (isRunning) MaterialTheme.typography.titleSmall
+ else MaterialTheme.typography.bodyMedium,
+ )
+ Text(
+ text = formatTaskPipelineTime(task.enqueuedAtMs),
+ modifier = Modifier.widthIn(min = 56.dp),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
val runningProgress = (task.state as? TaskRunState.Running)?.progress
val progressLabel = runningProgress?.label?.resolveText()
val stateLabel = when (val s = task.state) {
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
index a6776d3..9fca6c3 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
@@ -13,6 +13,7 @@ import com.github.nullptroma.wallenc.ui.resources.resolve
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
+import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
import com.github.nullptroma.wallenc.usecases.StorageSyncCompatibilityInput
import com.github.nullptroma.wallenc.usecases.isStorageCompatibleWithGroup
@@ -209,7 +210,7 @@ class StorageSyncViewModel @Inject constructor(
fun runSyncNow() {
val started = runStorageSyncUseCase.enqueue(
displayTitle = uiStrings(R.string.task_title_storage_sync),
- logReason = "sync-tab",
+ reason = StorageSyncTriggerReason.SyncTab,
)
if (!started) {
updateState(
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index 24c851b..238e51c 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -289,9 +289,12 @@
Синхронизация: группа «%1$s» — продление блокировок
Синхронизация: группа «%1$s» — не удалось продлить блокировку
%1$d / %2$d
- Синхронизация хранилищ запущена
- Синхронизация хранилищ завершена
- Синхронизация не удалась: %1$s
+ Синхронизация хранилищ запущена (%1$s)
+ Синхронизация хранилищ завершена (%1$s)
+ Синхронизация не удалась (%1$s): %2$s
+ debounce
+ sync-tab
+ background
Перечисление файлов и папок…
Создание хранилища…
Хранилище создано
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index fa35c6d..3018961 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -289,9 +289,12 @@
Storage sync: group "%1$s" renewing locks
Storage sync: group "%1$s" lock renewal failed
%1$d / %2$d
- Storage sync started
- Storage sync finished
- Storage sync failed: %1$s
+ Storage sync started (%1$s)
+ Storage sync finished (%1$s)
+ Storage sync failed (%1$s): %2$s
+ debounce
+ sync-tab
+ background
Enumerating files and directories…
Creating storage…
Storage created
diff --git a/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/RunStorageSyncUseCase.kt b/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/RunStorageSyncUseCase.kt
index 51b241a..98f933e 100644
--- a/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/RunStorageSyncUseCase.kt
+++ b/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/RunStorageSyncUseCase.kt
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.usecases
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
+import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.domain.tasks.TaskId
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
@@ -31,10 +32,10 @@ class RunStorageSyncUseCase @Inject constructor(
/**
* @param displayTitle заголовок задачи в UI (локализованный на стороне вызова)
- * @param logReason техническая метка для логов (не для UI)
+ * @param reason источник запуска — попадает в лог пайплайна
* @return false, если синхронизация уже в очереди или выполняется — новая задача не создана
*/
- fun enqueue(displayTitle: String, logReason: String): Boolean {
+ fun enqueue(displayTitle: String, reason: StorageSyncTriggerReason): Boolean {
if (!running.compareAndSet(false, true)) {
return false
}
@@ -45,36 +46,68 @@ class RunStorageSyncUseCase @Inject constructor(
dispatcher = Dispatchers.IO,
work = { ctx ->
try {
- ctx.log(TaskLogLevel.Info, TaskLogKey.SyncStarted)
- ctx.reportProgress(null, TaskProgressLabel.SyncStarted)
- syncEngine.syncAllGroups { fraction, label ->
- ctx.reportProgress(fraction, label)
- }
- ctx.log(TaskLogLevel.Info, TaskLogKey.SyncFinished)
- ctx.reportProgress(null, TaskProgressLabel.SyncCompleted)
+ executeSync(
+ reason = reason,
+ reportProgress = { fraction, label ->
+ ctx.reportProgress(fraction, label)
+ },
+ log = { level, key -> ctx.log(level, key) },
+ )
} catch (e: Exception) {
- val err = e.toWallencException()
- ctx.log(TaskLogLevel.Error, TaskLogKey.SyncFailed(err))
- ctx.fail(err)
+ ctx.fail(e.toWallencException())
} finally {
- running.set(false)
- _syncRunning.value = false
- _activeSyncTaskId.value = null
+ clearRunningState()
}
},
)
_activeSyncTaskId.value = taskId
return true
} catch (t: Throwable) {
- running.set(false)
- _syncRunning.value = false
- _activeSyncTaskId.value = null
+ clearRunningState()
throw t
}
}
- suspend fun runBlocking() {
+ suspend fun runBlocking(reason: StorageSyncTriggerReason) {
+ if (!running.compareAndSet(false, true)) {
+ return
+ }
+ _syncRunning.value = true
+ try {
+ executeSync(
+ reason = reason,
+ reportProgress = { _, _ -> },
+ log = { level, key -> orchestrator.appendPipelineLog(level, key) },
+ )
+ } finally {
+ clearRunningState()
+ }
+ }
+
+ private suspend fun executeSync(
+ reason: StorageSyncTriggerReason,
+ reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
+ log: (TaskLogLevel, TaskLogKey) -> Unit,
+ ) {
syncReadiness.awaitReady()
- syncEngine.syncAllGroups()
+ log(TaskLogLevel.Info, TaskLogKey.SyncStarted(reason))
+ reportProgress(null, TaskProgressLabel.SyncStarted)
+ try {
+ syncEngine.syncAllGroups { fraction, label ->
+ reportProgress(fraction, label)
+ }
+ log(TaskLogLevel.Info, TaskLogKey.SyncFinished(reason))
+ reportProgress(null, TaskProgressLabel.SyncCompleted)
+ } catch (e: Exception) {
+ val err = e.toWallencException()
+ log(TaskLogLevel.Error, TaskLogKey.SyncFailed(err, reason))
+ throw e
+ }
+ }
+
+ private fun clearRunningState() {
+ running.set(false)
+ _syncRunning.value = false
+ _activeSyncTaskId.value = null
}
}