car: right turn arrows and route times in map apps

Bitmap icons all shared one cache key, so Vela's turn arrows showed whatever
bitmap loaded first (a fuel pin). Route options now show the time and distance
apps send as spans instead of a lone dot.
This commit is contained in:
Seb3thehacker committed 2026-10-24 13:53:05 -07:00
1 parent e54fb08fc9
commit 1c4b472ce0
1 file changed
+46 -4
@@ -31,7 +31,41 @@ import kotlin.math.roundToInt
*/
/** The app's text, preferring the longest variant that will fit; variants are ordered longest first. */
fun CarText?.text(): String = this?.takeIf { !it.isEmpty }?.toCharSequence()?.toString().orEmpty()
fun CarText?.text(): String {
val chars = this?.takeIf { !it.isEmpty }?.toCharSequence() ?: return ""
return chars.withSpansFilledIn()
}
/**
* Apps write a route's time and length as spans over placeholder characters ("\u0000 · \u0000"
* with a DurationSpan and a DistanceSpan), leaving the host to format them. Plain toString()
* keeps only the placeholders, which is how a route option ended up reading "·".
*/
private fun CharSequence.withSpansFilledIn(): String {
if (this !is android.text.Spanned) return toString()
val spans = getSpans(0, length, androidx.car.app.model.CarSpan::class.java)
.filter { it is androidx.car.app.model.DurationSpan || it is androidx.car.app.model.DistanceSpan }
.sortedBy { getSpanStart(it) }
if (spans.isEmpty()) return toString()
val out = StringBuilder()
var at = 0
spans.forEach { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
if (start < at || end > length) return@forEach
out.append(this, at, start)
out.append(
when (span) {
is androidx.car.app.model.DurationSpan -> remainingTime(span.durationSeconds)
is androidx.car.app.model.DistanceSpan -> span.distance.display()
else -> ""
},
)
at = end
}
out.append(this, at, length)
return out.toString().replace("\u0000", "").trim()
}
/**
* Icons keep coming back under fresh [CarIcon]/IconCompat objects - every template refetch
@@ -53,7 +87,7 @@ private val iconCache = android.util.LruCache<String, ImageBitmap>(200)
fun CarIcon?.image(context: Context): ImageBitmap? {
val icon = this?.icon ?: return null
val key = icon.cacheKey(context)
iconCache.get(key)?.let { return it }
key?.let { k -> iconCache.get(k)?.let { return it } }
val drawable: Drawable = runCatching { icon.loadDrawable(context) }
.onFailure { GearslipLog.w("icon: loadDrawable threw for ${icon.describe()}: ${it.javaClass.simpleName}: ${it.message}") }
.getOrNull()
@@ -73,7 +107,7 @@ fun CarIcon?.image(context: Context): ImageBitmap? {
val bitmap = runCatching { drawable.toBitmap(width, height).asImageBitmap() }
.onFailure { GearslipLog.w("icon: toBitmap threw for ${icon.describe()}: ${it.javaClass.simpleName}: ${it.message}") }
.getOrNull() ?: return null
iconCache.put(key, bitmap)
key?.let { iconCache.put(it, bitmap) }
return bitmap
}
@@ -83,7 +117,15 @@ fun CarIcon?.image(context: Context): ImageBitmap? {
* so a cache keyed on the bare id kept drawing the old build's picture on the wrong button (Vela's
* new Saved button came up as its search glyph).
*/
private fun androidx.core.graphics.drawable.IconCompat.cacheKey(context: Context): String {
/**
* Null for an icon that carries its own pixels: two different bitmaps describe the same, so a
* key would hand one app's turn arrow the picture of whatever bitmap was cached first.
*/
private fun androidx.core.graphics.drawable.IconCompat.cacheKey(context: Context): String? {
if (type == androidx.core.graphics.drawable.IconCompat.TYPE_BITMAP ||
type == androidx.core.graphics.drawable.IconCompat.TYPE_ADAPTIVE_BITMAP ||
type == androidx.core.graphics.drawable.IconCompat.TYPE_DATA
) return null
if (type != androidx.core.graphics.drawable.IconCompat.TYPE_RESOURCE) return describe()
val installed = runCatching { context.packageManager.getPackageInfo(resPackage, 0).lastUpdateTime }.getOrDefault(0L)
return "${describe()}@$installed"