Coverage / app/src/main/java/io/oxygenhub/android/widget/HealthStrip.kt 40.6%
13 of 32 lines run, 3 of 5 functions, 14 of 30 branches · run #1664 on main · run missed
| 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 | } |