OXYGENHUB
# 943c54cf6a56 oxygenhub-android / app / src / main / java / io / oxygenhub / android / widget / HealthStrip.kt
2.9 KiB · last changed The widget: Needs you (the top review and newest assigned issue) and a Flow health strip of the last 30 runs; the mark in its own colours (closes #33) 3 days ago source blame history raw
1
package io.oxygenhub.android.widget
2
3
import android.graphics.Bitmap
4
import android.graphics.Canvas
5
import android.graphics.Paint
6
import android.graphics.RectF
7
import androidx.compose.ui.graphics.toArgb
8
import io.oxygenhub.android.ui.theme.DarkBad
9
import io.oxygenhub.android.ui.theme.DarkOk
10
import io.oxygenhub.android.ui.theme.DarkWarn
11
import io.oxygenhub.android.ui.theme.LightBad
12
import io.oxygenhub.android.ui.theme.LightOk
13
import io.oxygenhub.android.ui.theme.LightWarn
14
15
/**
16
 * The widget's Flow health strip (#33): your last runs, oldest on the left,
17
 * a bar each in its outcome's colour, and how many of the finished ones
18
 * passed. Drawn as one picture, as a widget's row holds only ten children.
19
 */
20
object HealthStrip {
21
    /** The kinds a run's status comes to. */
22
    enum class Kind { Passed, Failed, Live, Other }
23
24
    fun kind(status: String): Kind = when (status) {
25
        "passed" -> Kind.Passed
26
        "failed", "error" -> Kind.Failed
27
        "running", "queued" -> Kind.Live
28
        else -> Kind.Other
29
    }
30
31
    /** "93% of 28 finished runs passed", or null with nothing finished to count. Cancelled runs do not count. */
32
    fun summary(statuses: List<String>): String? {
33
        val kinds = statuses.map(::kind)
34
        val finished = kinds.count { it == Kind.Passed || it == Kind.Failed }
35
        if (finished == 0) return null
36
        val passed = kinds.count { it == Kind.Passed }
37
        val pct = (passed * 100 + finished / 2) / finished
38
        return if (finished == 1) "$pct% of the last finished run passed" else "$pct% of the last $finished finished runs passed"
39
    }
40
41
    /** The strip for [statuses] (newest first, as the hub lists them) at [width] × [height] pixels. */
42
    fun render(statuses: List<String>, width: Int, height: Int, density: Float, dark: Boolean): Bitmap {
43
        val bitmap = Bitmap.createBitmap(width.coerceAtLeast(1), height.coerceAtLeast(1), Bitmap.Config.ARGB_8888)
44
        val canvas = Canvas(bitmap)
45
        val runs = statuses.take(MAX).reversed()
46
        if (runs.isEmpty()) return bitmap
47
        val gap = 2f * density
48
        val bar = ((width - gap * (MAX - 1)) / MAX).coerceAtLeast(1f)
49
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
50
        // Right-aligned, so the newest run is always at the right edge.
51
        var x = width - runs.size * bar - (runs.size - 1) * gap
52
        for (s in runs) {
53
            paint.color = colour(kind(s), dark)
54
            canvas.drawRoundRect(RectF(x, 0f, x + bar, height.toFloat()), 1.5f * density, 1.5f * density, paint)
55
            x += bar + gap
56
        }
57
        return bitmap
58
    }
59
60
    private fun colour(k: Kind, dark: Boolean): Int = when (k) {
61
        Kind.Passed -> (if (dark) DarkOk else LightOk).toArgb()
62
        Kind.Failed -> (if (dark) DarkBad else LightBad).toArgb()
63
        Kind.Live -> (if (dark) DarkWarn else LightWarn).toArgb()
64
        Kind.Other -> if (dark) 0x66FFFFFF else 0x55000000
65
    }
66
67
    const val MAX = 30
68
}