Skip to content

Commit

Permalink
Show feed icons
Browse files Browse the repository at this point in the history
  • Loading branch information
hufman committed Jan 28, 2024
1 parent 153546e commit 881a4b1
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 19 deletions.
60 changes: 60 additions & 0 deletions app/src/gestalt/java/io/bimmergestalt/reader/GraphicsUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.bimmergestalt.reader

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.toDrawable
import coil.imageLoader
import coil.request.ImageRequest
import java.io.ByteArrayOutputStream

class GraphicsUtils(val context: Context) {
val imageLoader = context.imageLoader

suspend fun loadImageUri(uri: String?, width: Int, height: Int): Drawable? {
uri ?: return null
return if (Regex("^image/[a-rt-z-]*;base64,.*").matches(uri)) {
loadBase64Drawable(uri)
} else if (uri.startsWith("http")) {
val request = ImageRequest.Builder(context)
.data(uri)
.allowHardware(false)
.size(width, height)
.build()
imageLoader.execute(request).drawable
} else {
null
}
}

fun loadBase64Drawable(base64Uri: String): Drawable? {
val bytes = base64ToBytes(base64Uri)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).toDrawable(context.resources)
}

private fun base64ToBytes(base64String: String): ByteArray {
val base64Data = base64String.substringAfter("base64,")
return Base64.decode(base64Data, Base64.DEFAULT)
}

fun resizeDrawable(drawable: Drawable, width: Int, height: Int): Bitmap {
if (drawable is BitmapDrawable && drawable.bitmap.width == width && drawable.bitmap.height == height) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, width, height)
drawable.draw(canvas)
return bitmap
}

fun compressBitmapJpg(bitmap: Bitmap, quality: Int): ByteArray {
val jpg = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, jpg)
return jpg.toByteArray()
}
}
14 changes: 13 additions & 1 deletion app/src/gestalt/java/io/bimmergestalt/reader/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.bimmergestalt.reader

import androidx.core.text.HtmlCompat
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope

object Utils {
fun parseHtml(article: String): String {
Expand All @@ -20,4 +23,13 @@ object Utils {
.map { it.trim() }
.filter { it.isNotBlank() }
}
}

// https://stackoverflow.com/a/74207113/169035
val <A> Iterable<A>.par get() = ParallelizedIterable(this)
@JvmInline
value class ParallelizedIterable<A>(val iter: Iterable<A>) {
suspend fun <B> map(f: suspend (A) -> B): List<B> = coroutineScope {
iter.map { async { f(it) } }.awaitAll()
}
}
}
5 changes: 3 additions & 2 deletions app/src/gestalt/java/io/bimmergestalt/reader/carapp/CarApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationIdempotent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationSynchronized
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIState
import io.bimmergestalt.reader.GraphicsUtils
import io.bimmergestalt.reader.carapp.views.FeedView
import io.bimmergestalt.reader.carapp.views.HomeView
import io.bimmergestalt.reader.carapp.views.ReadView
Expand All @@ -27,7 +28,7 @@ import me.ash.reader.domain.service.RssService
const val TAG = "ReaderGestalt"
class CarApp(val iDriveConnectionStatus: IDriveConnectionStatus, securityAccess: SecurityAccess,
val carAppResources: CarAppSharedAssetResources,
val rssService: RssService, workManager: WorkManager
val rssService: RssService, workManager: WorkManager, graphicsUtils: GraphicsUtils
) {

val carConnection: BMWRemotingServer
Expand All @@ -52,7 +53,7 @@ class CarApp(val iDriveConnectionStatus: IDriveConnectionStatus, securityAccess:
readoutController = ReadoutController.build(carApp, "News")
val destStateId = carApp.components.values.filterIsInstance<RHMIComponent.EntryButton>().first().getAction()?.asHMIAction()?.target!!
homeView = HomeView(carApp.states[destStateId] as RHMIState, rssService, model)
feedView = FeedView(carApp.states[homeView.getFeedButtonDest()]!!, rssService, model)
feedView = FeedView(carApp.states[homeView.getFeedButtonDest()]!!, rssService, model, graphicsUtils)
readView = ReadView(carApp.states[homeView.getEntryListDest()] as RHMIState.ToolbarState, model)
readoutView = ReadoutView(carApp.states[readView.getReadoutDest()] as RHMIState.ToolbarState, readoutController, model)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.bimmergestalt.idriveconnectkit.android.CarAppAssetResources
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionReceiver
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus
import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess
import io.bimmergestalt.reader.GraphicsUtils
import io.bimmergestalt.reader.L
import me.ash.reader.domain.service.RssService
import javax.inject.Inject
Expand Down Expand Up @@ -78,7 +79,7 @@ class CarAppService: Service() {
iDriveConnectionStatus,
securityAccess,
CarAppSharedAssetResources(applicationContext, "news"),
rssService, workManager
rssService, workManager, GraphicsUtils(applicationContext)
)
}
thread?.start()
Expand Down
4 changes: 2 additions & 2 deletions app/src/gestalt/java/io/bimmergestalt/reader/carapp/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ data class FeedConfig(val groupId: String?, val feedId: String?,
}
val isPlaceholder = (groupId == null && feedId == null && !isStarred && !isUnread)
}
class FeedSelection(val name: String, val feedConfig: FeedConfig)
class FeedSelection(val name: String, val icon: String?, val feedConfig: FeedConfig)

class Model(workManager: WorkManager) {
val isSyncing = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
.asFlow().map { it.any { workInfo ->
workInfo.state == WorkInfo.State.RUNNING
} }
var feed = MutableStateFlow(FeedSelection(L.UNREAD, FeedConfig.UNREAD))
var feed = MutableStateFlow(FeedSelection(L.UNREAD, null, FeedConfig.UNREAD))
var articles = MutableStateFlow(emptyList<ArticleWithFeed>())
var articleIndex = MutableStateFlow(-1)
val article = articles.combine(articleIndex) { articles, index ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package io.bimmergestalt.reader.carapp.views

import android.util.Log
import de.bmw.idrive.BMWRemoting
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIActionListCallback
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIProperty
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIState
import io.bimmergestalt.reader.GraphicsUtils
import io.bimmergestalt.reader.L
import io.bimmergestalt.reader.Utils.par
import io.bimmergestalt.reader.carapp.FeedConfig
import io.bimmergestalt.reader.carapp.FeedSelection
import io.bimmergestalt.reader.carapp.Model
import io.bimmergestalt.reader.carapp.RHMIActionAbort
import io.bimmergestalt.reader.carapp.TAG
import kotlinx.coroutines.flow.collectLatest
import me.ash.reader.domain.service.RssService

class FeedView(state: RHMIState, val rssService: RssService, val model: Model): OnFocusedView(state) {
class FeedView(state: RHMIState, val rssService: RssService, val model: Model, val graphicsUtils: GraphicsUtils): OnFocusedView(state) {
val iconSize = 32
val feedList = state.componentsList.filterIsInstance<RHMIComponent.List>().first()
var feedOptions = emptyList<FeedSelection>()

fun initWidgets() {
feedList.setProperty(RHMIProperty.PropertyId.LIST_COLUMNWIDTH, "32,*")
feedList.getModel()?.value = RHMIModel.RaListModel.RHMIListConcrete(2).apply {
addRow(arrayOf("", L.UNREAD))
feedList.setProperty(RHMIProperty.PropertyId.LIST_COLUMNWIDTH, "32,${iconSize},*")
feedList.getModel()?.value = RHMIModel.RaListModel.RHMIListConcrete(3).apply {
addRow(arrayOf("", "", L.UNREAD))
}
feedList.getAction()?.asRAAction()?.rhmiActionCallback = RHMIActionListCallback { i ->
val option = feedOptions.getOrNull(i) ?: throw RHMIActionAbort()
Expand All @@ -44,28 +50,45 @@ class FeedView(state: RHMIState, val rssService: RssService, val model: Model):
.sortedBy { it.name }

val feedOptions = ArrayList<FeedSelection>(groups.size + feeds.size + 3)
feedOptions.add(FeedSelection(L.UNREAD, FeedConfig.UNREAD))
feedOptions.add(FeedSelection(L.STARRED, FeedConfig.STARRED))
feedOptions.add(FeedSelection(L.UNREAD, null, FeedConfig.UNREAD))
feedOptions.add(FeedSelection(L.STARRED, null, FeedConfig.STARRED))
feedOptions.addAll(groups.map {
FeedSelection(it.name, FeedConfig.GROUP(it.id))
FeedSelection(it.name, null, FeedConfig.GROUP(it.id))
})
feedOptions.add(FeedSelection(L.FEEDS, FeedConfig.PLACEHOLDER))
feedOptions.add(FeedSelection(L.FEEDS, null, FeedConfig.PLACEHOLDER))
feedOptions.addAll(feeds.map {
// TODO parse the url from it.icon like FeedIcon, which might be a base64 data
FeedSelection(it.name, FeedConfig.FEED(it.id))
FeedSelection(it.name, it.icon, FeedConfig.FEED(it.id))
})

feedList.getModel()?.value = object: RHMIModel.RaListModel.RHMIListAdapter<FeedSelection>(2, feedOptions) {
feedList.getModel()?.value = object: RHMIModel.RaListModel.RHMIListAdapter<FeedSelection>(3, feedOptions) {
override fun convertRow(index: Int, item: FeedSelection): Array<Any> {
return if (item.feedConfig.isPlaceholder) {
arrayOf("-", item.name)
arrayOf("-", "", item.name)
} else {
arrayOf("", item.name)
arrayOf("", "", item.name)
}
}
}
this.feedOptions = feedOptions
feedList.setProperty(RHMIProperty.PropertyId.LABEL_WAITINGANIMATION, false)

// now show the icons
feedList.getModel()?.value = RHMIModel.RaListModel.RHMIListConcrete(3).apply {
val rows: List<Array<Any>> = feedOptions.par.map { item ->
val heading = if (item.feedConfig.isPlaceholder) "-" else ""
val icon = graphicsUtils.loadImageUri(item.icon, iconSize, iconSize)?.let {
graphicsUtils.resizeDrawable(it, iconSize, iconSize)
}?.let {
graphicsUtils.compressBitmapJpg(it, 85)
}?.let {
BMWRemoting.RHMIResourceData(BMWRemoting.RHMIResourceType.IMAGEDATA, it)
} ?: ""
arrayOf(heading, icon, item.name)
}
rows.forEach {
addRow(it)
}
}
}
}
}

0 comments on commit 881a4b1

Please sign in to comment.