Skip to content

Commit

Permalink
Decode images as soon as they are received
Browse files Browse the repository at this point in the history
Simplifies the image widgets, which no longer need to handle ImageId/ImageData
  • Loading branch information
hufman committed May 5, 2024
1 parent 90cbd4c commit 5cde9be
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ fun Contents() {
color = background
) {
// Greeting("Android")
val homeScreen = if (Theme.appearance == Appearance.Material) AppListScreen else HomeScreen
val homeScreen = AppListScreen
// val homeScreen = if (Theme.appearance == Appearance.Material) AppListScreen else HomeScreen
Navigator(homeScreen) { navigator ->
Background(navigator)
SlideTransition(navigator) { screen ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import android.os.HandlerThread
import android.util.Log
import de.bmw.idrive.BMWRemoting
import de.bmw.idrive.BMWRemoting.RHMIDataTable
import de.bmw.idrive.BMWRemoting.RHMIResourceData
import de.bmw.idrive.BMWRemoting.RHMIResourceIdentifier
import de.bmw.idrive.BMWRemoting.RHMIResourceType
import de.bmw.idrive.BMWRemotingClient
import io.bimmergestalt.headunit.models.RHMIAppInfo
import io.bimmergestalt.headunit.models.RHMIApps
import io.bimmergestalt.headunit.models.RHMIEvent
import io.bimmergestalt.headunit.rhmi.RHMIResources
import io.bimmergestalt.headunit.utils.asEtchIntOrAny
import io.bimmergestalt.headunit.utils.decodeImage
import io.bimmergestalt.headunit.utils.isSameSize
import io.bimmergestalt.headunit.utils.merge
import io.bimmergestalt.headunit.utils.overlaps
Expand Down Expand Up @@ -82,28 +85,59 @@ class RHMIManager(val state: RHMIApps) {
eventHandlers.remove(handle)
}

/**
* Decode any images
*/
fun parseData(appId: String, value: Any?): Any? {
val app = state.knownApps[appId] ?: return value

return if (value is RHMIDataTable) {
val newValue = value.data.map { row ->
row.map {
parseData(appId, it)
}.toTypedArray()
}.toTypedArray()
RHMIDataTable(newValue, value.virtualTableEnable,
value.fromRow, value.numRows, value.totalRows,
value.fromColumn, value.numColumns, value.totalColumns)
} else if (value is RHMIResourceIdentifier && value.type == RHMIResourceType.IMAGEID) {
app.resources.imageDB[value.id]
} else if (value is RHMIResourceData && value.type == RHMIResourceType.IMAGEDATA) {
value.data.decodeImage()
} else if (value is ByteArray) {
value.decodeImage()
} else {
value
}
}

fun setData(appId: String, modelId: Int, value: Any?) {
if (value is RHMIDataTable) {
value.numRows = value.data.size // apps can lie about numRows and numCols
if (value.data.isNotEmpty()) {
value.numColumns = max(value.data.map { it.size })
val app = state.knownApps[appId] ?: return
val parsedValue = parseData(appId, value)
if (parsedValue is RHMIDataTable) {
parsedValue.numRows = parsedValue.data.size // apps can lie about numRows and numCols
if (parsedValue.data.isNotEmpty()) {
parsedValue.numColumns = max(parsedValue.data.map { it.size })
}
val existing = state.knownApps[appId]?.resources?.app?.getModel(modelId)
if (existing is RHMIDataTable && existing.isSameSize(value) && !value.overlaps(existing)) {
val existing = app.resources.app.getModel(modelId)
if (existing is RHMIDataTable && existing.isSameSize(parsedValue) && !parsedValue.overlaps(existing)) {
try {
// create a new object to trigger state tracking
val replacement = RHMIDataTable(existing.data, existing.virtualTableEnable,
existing.fromRow, existing.numRows, existing.totalRows,
existing.fromColumn, existing.numColumns, existing.totalColumns)
replacement.merge(value)
state.knownApps[appId]?.resources?.app?.setModel(modelId, replacement)
replacement.merge(parsedValue)
app.resources.app.setModel(modelId, replacement)
return
} catch (e: IllegalArgumentException) {
// unable to merge this table update, just replace like normal
}
}
app.resources.app.setModel(modelId, parsedValue)
} else {
app.resources.app.setModel(modelId, parsedValue?.asEtchIntOrAny())
}
state.knownApps[appId]?.resources?.app?.setModel(modelId, value?.asEtchIntOrAny())

}
fun setProperty(appId: String, componentId: Int, propertyId: Int, value: Any?) {
state.knownApps[appId]?.resources?.app?.setProperty(componentId, propertyId, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@ package io.bimmergestalt.headunit.models
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.asImageBitmap
import io.bimmergestalt.headunit.utils.decodeBitmap
import io.bimmergestalt.headunit.utils.decodeImage
import io.github.reactivecircus.cache4k.Cache
import java.security.MessageDigest

object ImageCache {
private val cache = Cache.Builder<Long, ImageTintable>().build()

private val empty = ImageTintable(ImageBitmap(1, 1, ImageBitmapConfig.Rgb565), false)
/**
* storeInCache should be set for ImageDB images
*/
suspend fun decodeImageBitmap(data: ByteArray, storeInCache: Boolean = false): ImageTintable? {
if (data.size > 75000) {
println("ImageCache ignoring too big ${data.size}")
return data.decodeBitmap()?.bitmap?.asImageBitmap()?.apply {
prepareToDraw()
}?.let {
ImageTintable(it, false)
}
return data.decodeImage()
}
val hash = MessageDigest.getInstance("MD5").digest(data)
val key = (hash[0].toLong() and 0xff shl 32+24) or
Expand All @@ -38,17 +34,10 @@ object ImageCache {
}
return if (storeInCache) {
cache.get(key) {
val bitmapInfo = data.decodeBitmap()
val image = bitmapInfo?.bitmap?.asImageBitmap()?.apply {
prepareToDraw()
} ?: ImageBitmap(1, 1, ImageBitmapConfig.Rgb565)
ImageTintable(image, bitmapInfo?.tintable ?: false)
data.decodeImage() ?: empty
}
} else {
cache.get(key) ?:
data.decodeBitmap()?.bitmap?.asImageBitmap()?.apply {
prepareToDraw()
}?.let { ImageTintable(it, false) }
data.decodeImage() ?: empty
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package io.bimmergestalt.headunit.rhmi

import android.util.Log
import androidx.compose.ui.graphics.asImageBitmap
import de.bmw.idrive.BMWRemoting
import io.bimmergestalt.headunit.models.ImageTintable
import io.bimmergestalt.headunit.utils.decodeAndCacheImage
import io.bimmergestalt.headunit.utils.decodeBitmap
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.util.zip.ZipInputStream
Expand Down Expand Up @@ -44,6 +41,14 @@ data class RHMIResources (
it != null
} as Map<Int, ImageTintable>

// convert any hardcoded ImageIds to their ImageBitmaps
app.modelStates.keys.forEach {
val value = app.modelStates[it]
if (value is BMWRemoting.RHMIResourceIdentifier && value.type == BMWRemoting.RHMIResourceType.IMAGEID) {
app.modelStates[it] = iconFiles[value.id]
}
}

return RHMIResources(app, textFiles, iconFiles)
}

Expand All @@ -67,37 +72,3 @@ data class RHMIResources (
}
}
}

fun loadImage(model: RHMIModel?, imageDB: Map<Int, ImageTintable>): ImageTintable? {
return when (model) {
is RHMIModel.ImageIdModel -> {
val imageId = model.imageId
imageDB[imageId]
}

is RHMIModel.RaImageModel -> {
val image = model.value
image?.decodeBitmap()?.let {
ImageTintable(it.bitmap.asImageBitmap(), it.tintable)
}
}

else -> null
}
}

fun loadText(model: RHMIModel?, textDB: Map<String, Map<Int, String>>): String {
return when (model) {
is RHMIModel.TextIdModel -> {
val dictionary = textDB["en-US"] ?: emptyMap() // TODO use context locale
val textId = model.textId
dictionary[textId] ?: ""
}

is RHMIModel.RaDataModel -> {
model.value as? String ?: ""
}

else -> ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fun RHMIAppEntry(app: RHMIAppInfo, entryButton: RHMIComponent.EntryButton, onCli
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
ImageModel(model = entryButton.getImageModel(), imageDB = app.resources.imageDB, modifier = Modifier
ImageModel(model = entryButton.getImageModel(), modifier = Modifier
.padding(4.dp)
.size(32.dp))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,33 @@
package io.bimmergestalt.headunit.ui.components

import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import de.bmw.idrive.BMWRemoting
import io.bimmergestalt.headunit.models.ImageTintable
import io.bimmergestalt.headunit.ui.screens.LocalImageDB
import io.bimmergestalt.headunit.ui.theme.Theme
import io.bimmergestalt.headunit.utils.decodeBitmap
import io.bimmergestalt.headunit.utils.loadImage
import io.bimmergestalt.headunit.utils.tintFilter
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel

@Composable
fun ImageModel(model: RHMIModel?, modifier: Modifier = Modifier, imageDB: Map<Int, ImageTintable> = LocalImageDB.current) {
if (model is RHMIModel.ImageIdModel) {
val imageId = model.imageId
val image = imageDB[imageId]
fun ImageModel(model: RHMIModel?, modifier: Modifier = Modifier) {
val image = loadImage(model)
if (image is ImageTintable) {
ImageBitmapNullable(image = image, contentDescription = null, modifier = modifier)
}
else if (model is RHMIModel.RaImageModel) {
val image = model.value
val bitmap = image?.decodeBitmap()
BitmapNullable(bitmap = bitmap?.bitmap, contentDescription = null, modifier = modifier)
} else {
Box(modifier = modifier)
}
}

@Composable
fun ImageCell(data: Any?, modifier: Modifier = Modifier, imageDB: Map<Int, ImageTintable> = LocalImageDB.current) {
if (data is ByteArray) {
val bitmap = data.decodeBitmap()
BitmapNullable(bitmap = bitmap?.bitmap, contentDescription = null, modifier = modifier)
} else if (data is BMWRemoting.RHMIResourceData && data.type == BMWRemoting.RHMIResourceType.IMAGEDATA) {
val bitmap = data.data.decodeBitmap()
BitmapNullable(bitmap = bitmap?.bitmap, contentDescription = null, modifier = modifier)
} else if (data is BMWRemoting.RHMIResourceIdentifier && data.type == BMWRemoting.RHMIResourceType.IMAGEID) {
val image = imageDB[data.id]
ImageBitmapNullable(image = image, contentDescription = null, modifier = modifier)
fun ImageCell(data: Any?, modifier: Modifier = Modifier) {
if (data is ImageTintable) {
ImageBitmapNullable(image = data, contentDescription = null, modifier = modifier)
}
}

@Composable
fun BitmapNullable(bitmap: Bitmap?, contentDescription: String?, modifier: Modifier = Modifier) {
if (bitmap != null) {
Image(bitmap.asImageBitmap(), contentDescription, modifier = modifier)
} else {
Box(modifier = modifier)
}
}
@Composable
fun ImageBitmapNullable(image: ImageTintable?, contentDescription: String?, modifier: Modifier = Modifier) {
if (image != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.bmw.idrive.BMWRemoting
import de.bmw.idrive.BMWRemoting.RHMIResourceIdentifier
import io.bimmergestalt.headunit.models.ImageTintable
import io.bimmergestalt.headunit.ui.theme.Theme
import io.bimmergestalt.headunit.utils.asBoolean
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIAction
Expand Down Expand Up @@ -74,10 +73,8 @@ fun List(component: RHMIComponent.List, modifier: Modifier = Modifier,

@Composable
fun Cell(data: Any?, modifier: Modifier = Modifier, richText: Boolean = false) {
val isByteArray = data is ByteArray
val isImageData = data is BMWRemoting.RHMIResourceData && data.type == BMWRemoting.RHMIResourceType.IMAGEDATA
val isImageId = data is RHMIResourceIdentifier && data.type == BMWRemoting.RHMIResourceType.IMAGEID
if (isByteArray || isImageData || isImageId) {
val isImage = data is ImageTintable
if (isImage) {
ImageCell(data, modifier.heightIn(48.dp, 96.dp))
} else {
val maxLines = if (richText) Int.MAX_VALUE else 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.bimmergestalt.headunit.ui.screens.LocalTextDB
import io.bimmergestalt.headunit.ui.theme.Theme
import io.bimmergestalt.headunit.utils.loadText
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel


@Composable
fun TextModel(model: RHMIModel?, modifier: Modifier = Modifier, textDB: Map<String, Map<Int, String>> = LocalTextDB.current) {
if (model is RHMIModel.TextIdModel) {

val dictionary = textDB["en-US"] ?: emptyMap() // TODO use context locale
val textId = model.textId
val text = dictionary[textId]
if (text != null) {
Text(text, modifier = modifier,
style = Theme.typography.headlineSmall,
color = Theme.colorScheme.primary)
} else {
Text("", modifier = modifier)
}
}
else if (model is RHMIModel.RaDataModel) {
val text = model.value as? String
if (text != null) {
Text(text, modifier = modifier,
style = Theme.typography.headlineSmall,
color = Theme.colorScheme.primary)
} else {
Text("", modifier = modifier)
}
} else {
Text("", modifier = modifier)
}
val text = loadText(model, textDB = textDB)
Text(text, modifier = modifier,
style = Theme.typography.headlineSmall,
color = Theme.colorScheme.primary)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import io.bimmergestalt.headunit.models.ImageTintable
import io.bimmergestalt.headunit.models.RHMIAppInfo
import io.bimmergestalt.headunit.rhmi.loadImage
import io.bimmergestalt.headunit.rhmi.loadText
import io.bimmergestalt.headunit.ui.components.Gauge
import io.bimmergestalt.headunit.ui.components.ImageModel
import io.bimmergestalt.headunit.ui.components.List
Expand All @@ -36,6 +34,8 @@ import io.bimmergestalt.headunit.ui.components.ToolbarSheet
import io.bimmergestalt.headunit.ui.components.ToolbarState
import io.bimmergestalt.headunit.ui.controllers.onClickAction
import io.bimmergestalt.headunit.utils.asBoolean
import io.bimmergestalt.headunit.utils.loadImage
import io.bimmergestalt.headunit.utils.loadText
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIAction
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIProperty
Expand Down Expand Up @@ -64,7 +64,6 @@ fun RHMIState(app: RHMIAppInfo, stateId: Int) {
}

CompositionLocalProvider(
LocalImageDB provides app.resources.imageDB,
LocalTextDB provides app.resources.textDB
) {
val navigator = LocalNavigator.currentOrThrow
Expand All @@ -74,7 +73,7 @@ fun RHMIState(app: RHMIAppInfo, stateId: Int) {
scope.launch { toolbarState.close() }
}
val toolbarEntries = state.toolbarComponentsList.map {
val icon = loadImage(it.getImageModel(), app.resources.imageDB)
val icon = loadImage(it.getImageModel())
val text = loadText(it.getTooltipModel(), app.resources.textDB)
ToolbarEntry(icon, text) { scope.launch { onClickAction(it.getAction(), null) }}
}
Expand Down Expand Up @@ -168,5 +167,4 @@ fun Component(app: RHMIAppInfo, component: RHMIComponent, layout: Int,
}
}

val LocalImageDB = staticCompositionLocalOf { emptyMap<Int, ImageTintable>() }
val LocalTextDB = staticCompositionLocalOf { emptyMap<String, Map<Int, String>>() }
Loading

0 comments on commit 5cde9be

Please sign in to comment.