Compare commits

..

3 Commits

5 changed files with 341 additions and 97 deletions

View File

@@ -13,21 +13,26 @@ import com.github.nullptroma.wallenc.app.R
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
import com.github.nullptroma.wallenc.domain.tasks.TaskId
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* Single foreground notification (one [NotificationManager.notify] id) that accumulates all task * Single foreground notification (one [NotificationManager.notify] id) that accumulates all task
* states. Multiple progress bars are implemented via [RemoteViews] — the standard * states. [orchestrator.foregroundUi] enqueues into [foregroundUiQueue] while [canPush] allows
* [NotificationCompat.Builder.setProgress] API only supports one bar per notification. * (at most one pending frame until the worker finishes an iteration). Indeterminate tasks: the
* worker reuses [lastUiState] when the queue is empty so dots advance without extra queue entries.
* Each handled state is followed by [NOTIFICATION_QUEUE_STEP_MS] before the next step.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class TaskPipelineForegroundService : Service() { class TaskPipelineForegroundService : Service() {
@@ -35,74 +40,78 @@ class TaskPipelineForegroundService : Service() {
@Inject @Inject
lateinit var orchestrator: ITaskOrchestrator lateinit var orchestrator: ITaskOrchestrator
private var repeat = false
private var canPush = true
private var lastUiState: TaskForegroundUiState? = null
/** Dot-animation phase per task (0…3); absent means 0 for first paint. */
private val indeterminateDotsPhaseByTaskId = ConcurrentHashMap<TaskId, Int>()
private val foregroundUiQueue = Channel<TaskForegroundUiState>(Channel.UNLIMITED)
private val serviceJob = SupervisorJob() private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(serviceJob + Dispatchers.Main.immediate) private val serviceScope = CoroutineScope(serviceJob + Dispatchers.Main.immediate)
override fun onBind(intent: android.content.Intent?): IBinder? = null override fun onBind(intent: android.content.Intent?): IBinder? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ensureChannel() ensureChannel()
startForeground(FOREGROUND_NOTIFICATION_ID, buildPlaceholderNotification()) startForeground(FOREGROUND_NOTIFICATION_ID, buildPlaceholderNotification())
val nm = getSystemService(NotificationManager::class.java)
serviceScope.launch { serviceScope.launch {
var sawVisible = false var sawVisible = false
var pendingTasks: List<TaskForegroundItem>? = null while (true) {
var lastNotificationAtMs = 0L if(repeat && !foregroundUiQueue.isEmpty || !repeat) {
var delayedFlushJob: Job? = null lastUiState = foregroundUiQueue.receive()
val nm = getSystemService(NotificationManager::class.java) }
canPush = true
fun pushVisibleTasks(tasks: List<TaskForegroundItem>) { val ui = lastUiState ?: continue
if (tasks.isEmpty()) return
nm.notify(FOREGROUND_NOTIFICATION_ID, buildAccumulatedNotification(tasks))
lastNotificationAtMs = System.currentTimeMillis()
}
orchestrator.foregroundUi.collect { ui ->
when (ui) { when (ui) {
is TaskForegroundUiState.Visible -> {
sawVisible = true
pendingTasks = ui.tasks
val now = System.currentTimeMillis()
val elapsed = now - lastNotificationAtMs
if (elapsed >= MIN_NOTIFICATION_UPDATE_INTERVAL_MS) {
delayedFlushJob?.cancel()
delayedFlushJob = null
pushVisibleTasks(ui.tasks)
pendingTasks = null
} else if (delayedFlushJob == null) {
delayedFlushJob = serviceScope.launch {
delay(MIN_NOTIFICATION_UPDATE_INTERVAL_MS - elapsed)
pendingTasks?.let { last ->
pushVisibleTasks(last)
pendingTasks = null
}
delayedFlushJob = null
}
}
}
TaskForegroundUiState.Hidden -> { TaskForegroundUiState.Hidden -> {
repeat = false
if (sawVisible) { if (sawVisible) {
delayedFlushJob?.cancel()
delayedFlushJob = null
pendingTasks?.let { last ->
pushVisibleTasks(last)
pendingTasks = null
}
nm.cancel(FOREGROUND_NOTIFICATION_ID) nm.cancel(FOREGROUND_NOTIFICATION_ID)
sawVisible = false indeterminateDotsPhaseByTaskId.clear()
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
return@launch
} }
} }
is TaskForegroundUiState.Visible -> {
if (ui.tasks.isNotEmpty()) {
sawVisible = true
nm.notify(
FOREGROUND_NOTIFICATION_ID,
buildAccumulatedNotification(ui.tasks),
)
}
// repeatedly receive Visible with same tasks while indeterminate dots animate
repeat = ui.tasks.any { it.progress?.fraction == null }
}
}
delay(NOTIFICATION_QUEUE_STEP_MS)
}
}
serviceScope.launch {
orchestrator.foregroundUi.collect { ui ->
if(canPush || ui is TaskForegroundUiState.Hidden) {
foregroundUiQueue.send(ui)
canPush = false
} }
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
foregroundUiQueue.close()
indeterminateDotsPhaseByTaskId.clear()
serviceScope.cancel() serviceScope.cancel()
super.onDestroy() super.onDestroy()
} }
@@ -153,6 +162,7 @@ class TaskPipelineForegroundService : Service() {
val color = notificationTemplateTextColor() val color = notificationTemplateTextColor()
for (i in 0 until MAX_TASK_ROWS) { for (i in 0 until MAX_TASK_ROWS) {
remoteViews.setTextColor(TASK_TITLE_IDS[i], color) remoteViews.setTextColor(TASK_TITLE_IDS[i], color)
remoteViews.setTextColor(TASK_SUBTITLE_IDS[i], color)
} }
} }
@@ -191,22 +201,56 @@ class TaskPipelineForegroundService : Service() {
val remaining = n - (MAX_TASK_ROWS - 1) val remaining = n - (MAX_TASK_ROWS - 1)
showOverflowRow(remoteViews, MAX_TASK_ROWS - 1, remaining) showOverflowRow(remoteViews, MAX_TASK_ROWS - 1, remaining)
} }
advanceIndeterminateDotPhasesAfterBind(sorted, n)
}
private fun advanceIndeterminateDotPhasesAfterBind(sorted: List<TaskForegroundItem>, n: Int) {
if (n == 0) return
val displayed =
if (n <= MAX_TASK_ROWS) sorted else sorted.take(MAX_TASK_ROWS - 1)
val presentIds = sorted.map { it.taskId }.toSet()
indeterminateDotsPhaseByTaskId.keys.retainAll { it in presentIds }
for (t in displayed) {
if (t.progress?.fraction == null) {
val id = t.taskId
indeterminateDotsPhaseByTaskId[id] =
((indeterminateDotsPhaseByTaskId[id] ?: 0) + 1) % 4
} else {
indeterminateDotsPhaseByTaskId.remove(t.taskId)
}
}
} }
private fun hideRow(remoteViews: RemoteViews, index: Int) { private fun hideRow(remoteViews: RemoteViews, index: Int) {
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.GONE) remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.GONE)
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.GONE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], "")
} }
private fun showTaskRow(remoteViews: RemoteViews, index: Int, task: TaskForegroundItem) { private fun showTaskRow(remoteViews: RemoteViews, index: Int, task: TaskForegroundItem) {
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE) remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE)
remoteViews.setTextViewText(TASK_TITLE_IDS[index], taskTitleText(task)) remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.VISIBLE)
remoteViews.setViewVisibility(TASK_PROGRESS_IDS[index], View.VISIBLE) remoteViews.setTextViewText(TASK_TITLE_IDS[index], task.title)
val label = task.progress?.label?.trim().orEmpty()
val fraction = task.progress?.fraction val fraction = task.progress?.fraction
if (fraction != null) { if (fraction != null) {
if (label.isNotEmpty()) {
remoteViews.setViewVisibility(TASK_SUBTITLE_IDS[index], View.VISIBLE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], label)
} else {
remoteViews.setViewVisibility(TASK_SUBTITLE_IDS[index], View.GONE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], "")
}
remoteViews.setViewVisibility(TASK_PROGRESS_IDS[index], View.VISIBLE)
val pct = (fraction.coerceIn(0f, 1f) * 100).roundToInt() val pct = (fraction.coerceIn(0f, 1f) * 100).roundToInt()
remoteViews.setProgressBar(TASK_PROGRESS_IDS[index], 100, pct, false) remoteViews.setProgressBar(TASK_PROGRESS_IDS[index], 100, pct, false)
} else { } else {
remoteViews.setProgressBar(TASK_PROGRESS_IDS[index], 0, 0, true) remoteViews.setViewVisibility(TASK_SUBTITLE_IDS[index], View.VISIBLE)
remoteViews.setTextViewText(
TASK_SUBTITLE_IDS[index],
indeterminateSubtitleWithDots(task.taskId, label),
)
remoteViews.setViewVisibility(TASK_PROGRESS_IDS[index], View.GONE)
} }
} }
@@ -216,19 +260,26 @@ class TaskPipelineForegroundService : Service() {
TASK_TITLE_IDS[index], TASK_TITLE_IDS[index],
getString(R.string.task_notification_more_tasks, remainingCount), getString(R.string.task_notification_more_tasks, remainingCount),
) )
remoteViews.setViewVisibility(TASK_PROGRESS_IDS[index], View.GONE) remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.GONE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], "")
} }
private fun taskTitleText(task: TaskForegroundItem): String { private fun indeterminateSubtitleWithDots(taskId: TaskId, baseLabel: String): String {
val label = task.progress?.label?.trim().orEmpty() val phase = (indeterminateDotsPhaseByTaskId[taskId] ?: 0) % 4
val title = task.title val dots = when (phase) {
return if (label.isNotEmpty()) "$title$label" else title 0 -> ""
1 -> "."
2 -> ".."
else -> "..."
}
return if (baseLabel.isEmpty()) dots else baseLabel + dots
} }
companion object { companion object {
private const val CHANNEL_ID = "wallenc_task_pipeline" private const val CHANNEL_ID = "wallenc_task_pipeline"
private const val FOREGROUND_NOTIFICATION_ID = 1001 private const val FOREGROUND_NOTIFICATION_ID = 1001
private const val MIN_NOTIFICATION_UPDATE_INTERVAL_MS = 500L
private const val NOTIFICATION_QUEUE_STEP_MS = 500L
/** Must match [R.layout.notification_wallenc_tasks_big] row count. */ /** Must match [R.layout.notification_wallenc_tasks_big] row count. */
private const val MAX_TASK_ROWS = 8 private const val MAX_TASK_ROWS = 8
@@ -265,5 +316,27 @@ class TaskPipelineForegroundService : Service() {
R.id.task_progress_6, R.id.task_progress_6,
R.id.task_progress_7, R.id.task_progress_7,
) )
private val TASK_SUBTITLE_IDS = intArrayOf(
R.id.task_subtitle_0,
R.id.task_subtitle_1,
R.id.task_subtitle_2,
R.id.task_subtitle_3,
R.id.task_subtitle_4,
R.id.task_subtitle_5,
R.id.task_subtitle_6,
R.id.task_subtitle_7,
)
private val TASK_LABEL_BAR_ROW_IDS = intArrayOf(
R.id.task_label_bar_row_0,
R.id.task_label_bar_row_1,
R.id.task_label_bar_row_2,
R.id.task_label_bar_row_3,
R.id.task_label_bar_row_4,
R.id.task_label_bar_row_5,
R.id.task_label_bar_row_6,
R.id.task_label_bar_row_7,
)
} }
} }

View File

@@ -23,12 +23,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_0" android:id="@+id/task_label_bar_row_0"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_0"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -47,12 +65,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_1" android:id="@+id/task_label_bar_row_1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -71,12 +107,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_2" android:id="@+id/task_label_bar_row_2"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_2"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -95,12 +149,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_3" android:id="@+id/task_label_bar_row_3"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_3"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -119,12 +191,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_4" android:id="@+id/task_label_bar_row_4"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_4"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -143,12 +233,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_5" android:id="@+id/task_label_bar_row_5"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_5"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -167,12 +275,30 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_6" android:id="@+id/task_label_bar_row_6"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_6"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -191,11 +317,29 @@
android:maxLines="2" android:maxLines="2"
android:textSize="13sp" /> android:textSize="13sp" />
<ProgressBar <LinearLayout
android:id="@+id/task_progress_7" android:id="@+id/task_label_bar_row_7"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" android:layout_height="wrap_content"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/task_subtitle_7"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textSize="11sp" />
<ProgressBar
android:id="@+id/task_progress_7"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="112dp"
android:layout_height="8dp"
android:layout_marginStart="2dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -1,7 +1,9 @@
package com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks package com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -10,6 +12,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -23,6 +26,7 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -45,6 +49,7 @@ fun TaskPipelineScreen(
val runningTaskIds = pipeline.runningTaskIds val runningTaskIds = pipeline.runningTaskIds
var showTestDialog by remember { mutableStateOf(false) } var showTestDialog by remember { mutableStateOf(false) }
var testDurationSec by remember { mutableFloatStateOf(10f) } var testDurationSec by remember { mutableFloatStateOf(10f) }
var testInfinity by remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@@ -128,12 +133,30 @@ fun TaskPipelineScreen(
onValueChange = { testDurationSec = it }, onValueChange = { testDurationSec = it },
valueRange = 0f..60f, valueRange = 0f..60f,
) )
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Checkbox(
checked = testInfinity,
onCheckedChange = { testInfinity = it },
)
Text(
stringResource(R.string.task_pipeline_test_dialog_infinity),
modifier = Modifier
.clickable { testInfinity = !testInfinity }
.weight(1f),
)
}
} }
}, },
confirmButton = { confirmButton = {
Button( Button(
onClick = { onClick = {
viewModel.startTestTask(testDurationSec.toInt()) viewModel.startTestTask(
testDurationSec.toInt(),
testInfinity,
)
showTestDialog = false showTestDialog = false
}, },
) { ) {

View File

@@ -14,10 +14,13 @@ class TaskPipelineViewModel @Inject constructor(
val orchestrator: ITaskOrchestrator, val orchestrator: ITaskOrchestrator,
) : ViewModel() { ) : ViewModel() {
fun startTestTask(durationSec: Int) { fun startTestTask(durationSec: Int, infinityIndeterminateProgress: Boolean) {
val safeDurationSec = durationSec.coerceIn(0, 60) val safeDurationSec = durationSec.coerceIn(0, 60)
val title =
if (infinityIndeterminateProgress) "Test task (${safeDurationSec}s, ∞)"
else "Test task (${safeDurationSec}s)"
orchestrator.enqueue( orchestrator.enqueue(
title = "Test task (${safeDurationSec}s)", title = title,
dispatcher = Dispatchers.Default, dispatcher = Dispatchers.Default,
work = { ctx -> work = { ctx ->
val steps = if (safeDurationSec == 0) 1 else safeDurationSec * 10 val steps = if (safeDurationSec == 0) 1 else safeDurationSec * 10
@@ -26,7 +29,7 @@ class TaskPipelineViewModel @Inject constructor(
val fraction = step.toFloat() / steps.toFloat() val fraction = step.toFloat() / steps.toFloat()
val elapsedMs = (fraction * safeDurationSec * 1000).toInt() val elapsedMs = (fraction * safeDurationSec * 1000).toInt()
ctx.reportProgress( ctx.reportProgress(
fraction = fraction, fraction = if (infinityIndeterminateProgress) null else fraction,
label = "Elapsed: ${elapsedMs / 1000}s / ${safeDurationSec}s", label = "Elapsed: ${elapsedMs / 1000}s / ${safeDurationSec}s",
) )
if (step < steps) delay(100) if (step < steps) delay(100)

View File

@@ -25,6 +25,7 @@
<string name="task_pipeline_test_dialog_duration">Duration: %1$d s</string> <string name="task_pipeline_test_dialog_duration">Duration: %1$d s</string>
<string name="task_pipeline_test_dialog_start">Start</string> <string name="task_pipeline_test_dialog_start">Start</string>
<string name="task_pipeline_test_dialog_cancel">Cancel</string> <string name="task_pipeline_test_dialog_cancel">Cancel</string>
<string name="task_pipeline_test_dialog_infinity">Infinity (indeterminate progress)</string>
<string name="task_state_queued">Queued</string> <string name="task_state_queued">Queued</string>
<string name="task_state_running">Running</string> <string name="task_state_running">Running</string>
<string name="task_state_completed">Completed</string> <string name="task_state_completed">Completed</string>