Coverage / app/src/main/java/io/oxygenhub/android/api/Avatar.kt 100.0%
12 of 12 lines run, 9 of 9 functions, 25 of 27 branches · run #1654 on main · run missed
| 1 | package io.oxygenhub.android.api | |
| 2 | ||
| 3 | /** | |
| 4 | * An account's picture as the hub serves it at `/avatar/{name}`: the | |
| 5 | * picture its owner uploaded, or else the identicon the hub draws from the | |
| 6 | * name. The identicon comes as SVG, which Android cannot draw by itself, so | |
| 7 | * it is read into its hue and cells and drawn natively. | |
| 8 | */ | |
| 9 | sealed interface Avatar { | |
| 10 | /** An uploaded PNG or JPEG, as served. */ | |
| 11 | class Picture(val bytes: ByteArray) : Avatar | |
| 12 | ||
| 13 | /** | |
| 14 | * The hub's identicon: on a 7 by 7 field of the hue at 45% saturation and | |
| 15 | * 92% lightness, the [cells] (x, y, each 1 to 5) at 65% and 45%. | |
| 16 | */ | |
| 17 | data class Identicon(val hue: Int, val cells: Set<Pair<Int, Int>>) : Avatar | |
| 18 | ||
| 19 | companion object { | |
| 20 | private val FIELD = Regex("""<rect width="7" height="7" fill="hsl\((\d{1,3}),45%,92%\)"/>""") | |
| 21 | private val CELL = Regex("""<rect x="([1-5])" y="([1-5])" width="1" height="1"/>""") | |
| 22 | ||
| 23 | /** | |
| 24 | * The answer to `/avatar/{name}` by its media type; null for what this | |
| 25 | * app cannot draw (a system account's own mark, or anything unexpected), | |
| 26 | * which then shows as the name's initial. | |
| 27 | */ | |
| 28 | fun parse(contentType: String?, bytes: ByteArray): Avatar? = | |
| 29 | when (contentType?.substringBefore(';')?.trim()?.lowercase()) { | |
| 30 | "image/png", "image/jpeg" -> if (bytes.isEmpty()) null else Picture(bytes) | |
| 31 | "image/svg+xml" -> identicon(bytes.decodeToString()) | |
| 32 | else -> null | |
| 33 | } | |
| 34 | ||
| 35 | /** The identicon in the hub's SVG, or null when the SVG is not one. */ | |
| 36 | fun identicon(svg: String): Identicon? { | |
| 37 | val hue = FIELD.find(svg)?.groupValues?.get(1)?.toIntOrNull()?.takeIf { it < 360 } ?: return null | |
| 38 | val cells = CELL.findAll(svg).map { it.groupValues[1].toInt() to it.groupValues[2].toInt() }.toSet() | |
| 39 | return Identicon(hue, cells) | |
| 40 | } | |
| 41 | } | |
| 42 | } |