Compare commits
3 Commits
75162e2d64
...
404ff201c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 404ff201c4 | |||
| e00455691a | |||
| be07ccf0aa |
@@ -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())
|
||||||
|
|
||||||
serviceScope.launch {
|
|
||||||
var sawVisible = false
|
|
||||||
var pendingTasks: List<TaskForegroundItem>? = null
|
|
||||||
var lastNotificationAtMs = 0L
|
|
||||||
var delayedFlushJob: Job? = null
|
|
||||||
val nm = getSystemService(NotificationManager::class.java)
|
val nm = getSystemService(NotificationManager::class.java)
|
||||||
|
|
||||||
fun pushVisibleTasks(tasks: List<TaskForegroundItem>) {
|
serviceScope.launch {
|
||||||
if (tasks.isEmpty()) return
|
var sawVisible = false
|
||||||
nm.notify(FOREGROUND_NOTIFICATION_ID, buildAccumulatedNotification(tasks))
|
while (true) {
|
||||||
lastNotificationAtMs = System.currentTimeMillis()
|
if(repeat && !foregroundUiQueue.isEmpty || !repeat) {
|
||||||
|
lastUiState = foregroundUiQueue.receive()
|
||||||
}
|
}
|
||||||
|
canPush = true
|
||||||
|
|
||||||
orchestrator.foregroundUi.collect { ui ->
|
val ui = lastUiState ?: continue
|
||||||
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,30 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_0"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_0"
|
android:id="@+id/task_progress_0"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_1"
|
android:id="@+id/task_progress_1"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_2"
|
android:id="@+id/task_progress_2"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_3"
|
android:id="@+id/task_progress_3"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_4"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_4"
|
android:id="@+id/task_progress_4"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_5"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_5"
|
android:id="@+id/task_progress_5"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_6"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_6"
|
android:id="@+id/task_progress_6"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/task_label_bar_row_7"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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
|
<ProgressBar
|
||||||
android:id="@+id/task_progress_7"
|
android:id="@+id/task_progress_7"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="112dp"
|
||||||
android:layout_height="8dp"
|
android:layout_height="8dp"
|
||||||
android:layout_marginTop="2dp" />
|
android:layout_marginStart="2dp" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user