From caba6c4ac71d229ee90a0eee70487161e8230faa Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Fri, 22 Aug 2025 20:36:17 +0200 Subject: [PATCH 1/8] provide function to load assets remote via remote data source --- .../sdk/assetservice/data/dto/AssetDto.kt | 21 +++ .../assetservice/data/dto/AssetVariantDto.kt | 8 + .../sdk/assetservice/data/dto/ManifestDto.kt | 7 + .../sdk/assetservice/data/dto/VariantDto.kt | 20 +++ .../data/remote/RemoteAssetsSource.kt | 148 ++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt new file mode 100644 index 000000000..7b6214bb3 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt @@ -0,0 +1,21 @@ +package io.snabble.sdk.assetservice.data.dto + +data class AssetDto( + val data: ByteArray, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AssetDto + + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + return data.contentHashCode() + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt new file mode 100644 index 000000000..fd794e165 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt @@ -0,0 +1,8 @@ +package io.snabble.sdk.assetservice.data.dto + +import com.google.gson.annotations.SerializedName + +data class AssetVariantDto( + @SerializedName("name") var name: String, + @SerializedName("variants") var variants: MutableMap = mutableMapOf() +) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt new file mode 100644 index 000000000..e038fb7fc --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt @@ -0,0 +1,7 @@ +package io.snabble.sdk.assetservice.data.dto + +import com.google.gson.annotations.SerializedName + +data class ManifestDto( + @SerializedName("files") val files: List +) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt new file mode 100644 index 000000000..a647c0be7 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt @@ -0,0 +1,20 @@ +package io.snabble.sdk.assetservice.data.dto + +import com.google.gson.annotations.SerializedName + +enum class VariantDto(var factor: String?, var density: Float) { + @SerializedName("1x") + MDPI(factor = "1x", density = 1.0f), + + @SerializedName("1.5x") + HDPI(factor = "1.5x", density = 1.5f), + + @SerializedName("2x") + XHDPI(factor = "2x", density = 2.0f), + + @SerializedName("3x") + XXHDPI(factor = "3x", density = 3.0f), + + @SerializedName("4x") + XXXHDPI(factor = "4x", density = 4.0f); +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt new file mode 100644 index 000000000..6eef64125 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt @@ -0,0 +1,148 @@ +package io.snabble.sdk.assetservice.data.remote + +import com.google.gson.JsonSyntaxException +import io.snabble.sdk.Project +import io.snabble.sdk.Snabble +import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.data.dto.AssetVariantDto +import io.snabble.sdk.assetservice.data.dto.ManifestDto +import io.snabble.sdk.assetservice.data.dto.VariantDto +import io.snabble.sdk.utils.GsonHolder +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import okhttp3.CacheControl +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Request +import okhttp3.Response +import okio.IOException +import org.apache.commons.io.FilenameUtils +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.time.Duration.Companion.seconds + +interface RemoteAssetsSource { + + suspend fun downloadManifestForProject(project: Project): ManifestDto? + + suspend fun downloadAllAssets(project: Project, files: List): List +} + +class RemoteAssetsSourceImpl : RemoteAssetsSource { + + override suspend fun downloadManifestForProject(project: Project): ManifestDto? = + with(Dispatchers.IO) { + suspendCancellableCoroutine { continuation: Continuation -> + val assetsUrl = project.assetsUrl ?: return@suspendCancellableCoroutine + + val request = Request.Builder() + .cacheControl(CacheControl.Builder().maxAge(30.seconds).build()) + .url(assetsUrl) + .get() + .build() + + project.okHttpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Logger.e(e.message) + continuation.resume(null) + } + + override fun onResponse(call: Call, response: Response) { + try { + if (!response.isSuccessful) { + continuation.resume(null) + return + } + + val manifestDto = GsonHolder.get().fromJson(response.body.string(), ManifestDto::class.java) + continuation.resume(manifestDto) + } catch (e: JsonSyntaxException) { + Logger.e("Manifest download failed: ${e.message}") + continuation.resume(null) + } finally { + response.close() + } + } + } + ) + } + } + + // Todo: filter if the existing assets out + override suspend fun downloadAllAssets( + project: Project, + files: List + ) = withContext(Dispatchers.IO) { + + val assetsUrls = files.mapNotNull { asset -> + val url: String? = asset.variants[VariantDto.MDPI] + val format = FilenameUtils.getExtension(asset.name) + when { + format.isValidFormat() && asset.name.isRootAsset() -> url + else -> null + } + } + + val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) + + assetsUrls.map { url -> + async { + semaphore.withPermit { + loadAsset(project, url) + } + } + }.awaitAll() + } + + private suspend fun loadAsset(project: Project, url: String): AssetDto? = withContext(Dispatchers.IO) { + suspendCancellableCoroutine { continuation -> + val request = Request.Builder() + .url(Snabble.absoluteUrl(url)) + .get() + .build() + + val call = project.okHttpClient.newCall(request) + + // Handle cancellation + continuation.invokeOnCancellation { + call.cancel() + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resume(null) + } + + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + continuation.resume(null) + return + } + + val bytes = response.body.bytes() + + val asset = AssetDto(data = bytes) + + continuation.resume(asset) + response.close() + } + }) + } + } + + private fun String.isValidFormat() = VALID_FORMATS.contains(this) + + private fun String.isRootAsset() = !contains("/") + + companion object { + + private val VALID_FORMATS = listOf("svg", "jpg", "webp") + private const val MAX_CONCURRENT_REQUESTS = 10 + } +} From d0c905a9f508715e554c91aace00111b381fa586 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Tue, 26 Aug 2025 13:52:03 +0200 Subject: [PATCH 2/8] WIP: rewrite asset service --- core/src/main/java/io/snabble/sdk/Project.kt | 10 +- .../snabble/sdk/assetservice/AssetService.kt | 132 +++++++++++ .../assetservice/data/AssetsRepositoryImpl.kt | 38 ++++ .../assetservice/data/ImageRepositoryImpl.kt | 81 +++++++ .../data/local/LocalAssetDataSource.kt | 209 ++++++++++++++++++ .../data/local/image/LocalDiskDataSource.kt | 144 ++++++++++++ .../data/local/image/LocalMemoryDataSource.kt | 89 ++++++++ .../data/remote/RemoteAssetsSource.kt | 120 ++++++---- .../assetservice/domain/AssetsRepository.kt | 12 + .../assetservice/domain/ImageRepository.kt | 9 + .../sdk/assetservice/domain/model/Type.kt | 11 + .../sdk/assetservice/domain/model/UiMode.kt | 7 + 12 files changed, 818 insertions(+), 44 deletions(-) create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt diff --git a/core/src/main/java/io/snabble/sdk/Project.kt b/core/src/main/java/io/snabble/sdk/Project.kt index 0c53437d1..833bc3c20 100644 --- a/core/src/main/java/io/snabble/sdk/Project.kt +++ b/core/src/main/java/io/snabble/sdk/Project.kt @@ -5,6 +5,8 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.reflect.TypeToken +import io.snabble.sdk.assetservice.AssetService +import io.snabble.sdk.assetservice.assetServiceFactory import io.snabble.sdk.auth.SnabbleAuthorizationInterceptor import io.snabble.sdk.checkout.Checkout import io.snabble.sdk.codes.templates.CodeTemplate @@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor import org.apache.commons.lang3.LocaleUtils import java.io.File import java.math.RoundingMode @@ -357,6 +360,9 @@ class Project internal constructor( lateinit var assets: Assets private set + lateinit var assetService: AssetService + private set + var appTheme: AppTheme? = null private set @@ -548,6 +554,7 @@ class Project internal constructor( .newBuilder() .addInterceptor(SnabbleAuthorizationInterceptor(this)) .addInterceptor(AcceptedLanguageInterceptor()) + .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() _shoppingCart.tryEmit(ShoppingCart(this)) @@ -567,6 +574,8 @@ class Project internal constructor( assets = Assets(this) + assetService = assetServiceFactory(project = this, context = Snabble.application) + googlePayHelper = paymentMethodDescriptors .mapNotNull { it.paymentMethod } .firstOrNull { it == PaymentMethod.GOOGLE_PAY } @@ -579,7 +588,6 @@ class Project internal constructor( coupons.setProjectCoupons(couponList) } coupons.update() - notifyUpdate() } diff --git a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt new file mode 100644 index 000000000..d9a5c9604 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt @@ -0,0 +1,132 @@ +package io.snabble.sdk.assetservice + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.util.DisplayMetrics +import com.caverock.androidsvg.SVG +import io.snabble.sdk.Project +import io.snabble.sdk.assetservice.data.AssetsRepositoryImpl +import io.snabble.sdk.assetservice.data.ImageRepositoryImpl +import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.data.local.LocalAssetDataSourceImpl +import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSourceImpl +import io.snabble.sdk.assetservice.data.local.image.LocalMemorySourceImpl +import io.snabble.sdk.assetservice.data.remote.RemoteAssetsSourceImpl +import io.snabble.sdk.assetservice.domain.AssetsRepository +import io.snabble.sdk.assetservice.domain.ImageRepository +import io.snabble.sdk.assetservice.domain.model.Type +import io.snabble.sdk.assetservice.domain.model.UiMode +import io.snabble.sdk.extensions.xx +import io.snabble.sdk.utils.Logger +import java.io.InputStream +import kotlin.math.roundToInt + +interface AssetService { + + suspend fun updateAllAssets() + + suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap? +} + +class AssetServiceImpl( + private val displayMetrics: DisplayMetrics, + private val assetRepository: AssetsRepository, + private val imageRepository: ImageRepository, +) : AssetService { + + override suspend fun updateAllAssets() { + assetRepository.updateAllAssets() + } + + override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap? { + val bitmap = when (val bitmap = imageRepository.getBitmap(key = name)) { + null -> createBitmap(name, type, uiMode) + else -> bitmap + } ?: return null + + //Save converted bitmap + imageRepository.putBitmap(name, bitmap) + + return bitmap + } + + private fun createSVGBitmap(data: InputStream): Bitmap? { + val svg = SVG.getFromInputStream(data) + return try { + + val width = svg.getDocumentWidth() * displayMetrics.density + val height = svg.getDocumentHeight() * displayMetrics.density + + // Set the SVG's view box to the desired size + svg.setDocumentWidth(width) + svg.setDocumentHeight(height) + + // Create bitmap and canvas + val bitmap = androidx.core.graphics.createBitmap(width.roundToInt(), height.roundToInt()) + val canvas = Canvas(bitmap) + + // Render SVG to canvas + svg.renderToCanvas(canvas) + + bitmap + } catch (e: Exception) { + Logger.e("Error converting SVG to bitmap", e) + null + } + } + + private suspend fun createBitmap(name: String, type: Type, uiMode: UiMode): Bitmap? { + "create Bitmap" + val cachedAsset = + assetRepository.loadAsset(name = name, type = type, uiMode = uiMode).xx("loaded Asset") ?: return null + return when (type) { + Type.SVG -> createSVGBitmap(cachedAsset.data) + Type.JPG, + Type.WEBP -> BitmapFactory.decodeStream(cachedAsset.data) + } + } + + private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): AssetDto? { + assetRepository.updateAllAssets() + return assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) + } +} + +fun assetServiceFactory( + project: Project, + context: Context +): AssetService { + val localDiskDataSource = LocalDiskDataSourceImpl(storageDirectory = project.internalStorageDirectory) + val localMemoryDataSource = LocalMemorySourceImpl() + val imageRepository = ImageRepositoryImpl( + localMemoryDataSource = localMemoryDataSource, + localDiskDataSource = localDiskDataSource + ) + + val localAssetDataSource = LocalAssetDataSourceImpl(project) + val remoteAssetsSource = RemoteAssetsSourceImpl(project) + val assetRepository = AssetsRepositoryImpl( + remoteAssetsSource = remoteAssetsSource, + localAssetDataSource = localAssetDataSource + ) + + return AssetServiceImpl( + assetRepository = assetRepository, + imageRepository = imageRepository, + displayMetrics = context.resources.displayMetrics + ) +} + +fun Context.getUiMode() = if (isDarkMode()) UiMode.NIGHT else UiMode.DAY + +// Method 2: Extension function for cleaner usage +private fun Context.isDarkMode(): Boolean { + return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> true + Configuration.UI_MODE_NIGHT_NO -> false + else -> false + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt new file mode 100644 index 000000000..52a18a443 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt @@ -0,0 +1,38 @@ +package io.snabble.sdk.assetservice.data + +import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.data.dto.ManifestDto +import io.snabble.sdk.assetservice.data.local.LocalAssetDataSource +import io.snabble.sdk.assetservice.data.remote.RemoteAssetsSource +import io.snabble.sdk.assetservice.domain.AssetsRepository +import io.snabble.sdk.assetservice.domain.model.Type +import io.snabble.sdk.assetservice.domain.model.UiMode +import io.snabble.sdk.utils.Logger +import org.apache.commons.io.FilenameUtils + +class AssetsRepositoryImpl( + private val remoteAssetsSource: RemoteAssetsSource, + private val localAssetDataSource: LocalAssetDataSource +) : AssetsRepository { + + override suspend fun updateAllAssets() { + Logger.e("Start updating all assets...") + val manifest: ManifestDto = remoteAssetsSource.downloadManifestForProject() ?: return + val newAssets = localAssetDataSource.removeExistingAssets(manifest.files) + Logger.e("Filtered new assets $newAssets") + Logger.e("Continue with loading all new assets...") + val assets: List = remoteAssetsSource.downloadAllAssets(newAssets) + Logger.e("Saving new assets $assets locally...") + localAssetDataSource.saveMultipleAssets(assets = assets) + } + + override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? = + getLocalAsset(filename = name.createFileName(type, uiMode)) + + private suspend fun getLocalAsset(filename: String): AssetDto? = localAssetDataSource.loadAsset(filename) + + private fun String.createFileName(type: Type, uiMode: UiMode): String { + val cleanedName = FilenameUtils.removeExtension(this) + return "$cleanedName${uiMode.value}${type.value}" + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt new file mode 100644 index 000000000..63d0ce397 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt @@ -0,0 +1,81 @@ +package io.snabble.sdk.assetservice.data + +import android.graphics.Bitmap +import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSource +import io.snabble.sdk.assetservice.data.local.image.LocalMemoryDataSource +import io.snabble.sdk.assetservice.domain.ImageRepository +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ImageRepositoryImpl( + private val localMemoryDataSource: LocalMemoryDataSource, + private val localDiskDataSource: LocalDiskDataSource +) : ImageRepository { + + private val cacheScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + cacheScope.launch { + localMemoryDataSource.evictedItems.filterNotNull().collect { (key, bitmap) -> + localDiskDataSource.saveToDisk(key, bitmap) + } + } + } + + override suspend fun getBitmap(key: String): Bitmap? = withContext(Dispatchers.IO) { + + // 1. Check memory cache first (fastest ~0.1ms) + localMemoryDataSource.getBitmap(key)?.let { + return@withContext it + } + + // 2. Check disk cache (medium speed ~5-20ms) + localDiskDataSource.getBitmap(key)?.let { + // Save it in memory cache for faster access next time + localMemoryDataSource.putBitmap(key, it) + return@withContext it + } + + Logger.d("Image cache missing") + return@withContext null + } + + /** + * Manually put a bitmap in cache + */ + override suspend fun putBitmap(key: String, bitmap: Bitmap) { + localMemoryDataSource.putBitmap(key, bitmap) + localDiskDataSource.saveToDisk(key, bitmap) + } + + /** + * Clear memory cache (disk cache remains) + */ + fun clearMemoryCache() { + localMemoryDataSource.clearCache() + Logger.d("Memory cache cleared") + } + + /** + * Clear everything + */ + suspend fun clearAllCaches() = withContext(Dispatchers.IO) { + clearMemoryCache() + localDiskDataSource.clearCache() + Logger.d("All caches cleared") + } + + /** + * Clean up resources - call this in onDestroy + */ + fun close() { + cacheScope.cancel() + Logger.d("Cache manager closed") + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt new file mode 100644 index 000000000..9492a8769 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt @@ -0,0 +1,209 @@ +package io.snabble.sdk.assetservice.data.local + +import io.snabble.sdk.Project +import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.data.dto.AssetVariantDto +import io.snabble.sdk.extensions.xx +import io.snabble.sdk.utils.GsonHolder +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.commons.io.IOUtils +import java.io.File +import java.io.FileOutputStream + +interface LocalAssetDataSource { + + suspend fun loadAsset(name: String): AssetDto? + suspend fun saveMultipleAssets(assets: List) + suspend fun removeExistingAssets(assets: List): List +} + +class LocalAssetDataSourceImpl( + private val project: Project, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : LocalAssetDataSource { + + private val assetsDir = File(project.internalStorageDirectory, "assets/") + private val manifestFile = File(project.internalStorageDirectory, "assets_v2.json") + + private var manifest = Manifest() + + init { + assetsDir.mkdirs() + loadManifest() + } + + override suspend fun loadAsset(name: String): AssetDto? = withContext(dispatcher) { + try { + manifest.assets.contains(name).xx("contains $name") + val asset = manifest.assets[name] + ?: throw IllegalArgumentException("Asset '$name' not found in manifest") + + val file = File(asset.filePath) + if (!file.exists()) { + throw IllegalStateException("Asset file not found: ${asset.filePath}") + } + + return@withContext AssetDto( + name = name, + data = file.inputStream(), + hash = asset.hash + ) + } catch (e: Exception) { + Logger.e(e.message) + return@withContext null + } + } + + override suspend fun saveMultipleAssets(assets: List) = withContext(dispatcher) { + try { + assets.forEach { assetDto -> + val fileName = "${assetDto.hash}_${assetDto.name}" + val file = File(assetsDir, fileName) + file.createNewFile() + // Ensure assets directory exists + if (!assetsDir.exists()) { + assetsDir.mkdirs() + } + assetDto.data.use { inputStream -> + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + // Create asset entry + val asset = Asset( + filePath = file.absolutePath, + hash = assetDto.hash + ) + + withContext(Dispatchers.Main) { + "save asset ${assetDto.name}".xx() + manifest.assets[assetDto.name] = asset + } + } + + // Save manifest once after all assets + saveManifest() + } catch (e: Exception) { + e.xx("Fuak me ${e.cause}") + Logger.e(e.message) + } + } + + override suspend fun removeExistingAssets(assets: List): List = + assets.filterNot { manifest.assets.contains(it.name) } + + private suspend fun saveManifest() = withContext(dispatcher) { + try { + val jsonString = GsonHolder.get().toJson(manifest) + manifestFile.writeText(jsonString) + println("Saved manifest for project ${project.id}") + } catch (e: Exception) { + println("Could not write manifest: ${e.message}") + throw e + } + } + + private fun loadManifest() { + try { + if (manifestFile.exists()) { + val jsonString = manifestFile.readText() + manifest = GsonHolder.get().fromJson(jsonString, Manifest::class.java) + println("Loaded manifest for project ${project.id} with ${manifest.assets.size} assets") + } + } catch (e: Exception) { + println("Could not load manifest, creating new one: ${e.message}") + manifest = Manifest() + } + } + + fun listAssets(): List = manifest.assets.keys.toList() + + fun assetExists(name: String): Boolean = manifest.assets.containsKey(name) + + suspend fun deleteAsset(name: String): Result = withContext(dispatcher) { + runCatching { + val asset = manifest.assets[name] ?: return@runCatching false + + val file = File(asset.filePath) + val deleted = if (file.exists()) file.delete() else true + + if (deleted) { + manifest.assets.remove(name) + saveManifest() + } + + deleted + } + } + + suspend fun cleanupUnusedAssets(referencedHashes: Set): Result = withContext(dispatcher) { + runCatching { + val removals = mutableListOf() + var hasChanges = false + + // Find assets to remove + manifest.assets.forEach { (name, asset) -> + if (!referencedHashes.contains(asset.hash)) { + println("Removing unused asset: $name") + + // Delete file + val file = File(asset.filePath) + if (file.exists()) { + file.delete() + } + + removals.add(name) + hasChanges = true + } + } + + // Remove from manifest + removals.forEach { name -> + manifest.assets.remove(name) + } + + // Save manifest if changes were made + if (hasChanges) { + saveManifest() + println("Cleaned up ${removals.size} unused assets for project ${project.id}") + } + + removals.size + } + } + + suspend fun cleanupOrphanedFiles(): Result = withContext(dispatcher) { + runCatching { + val manifestFilePaths = manifest.assets.values.map { it.filePath }.toSet() + val orphanedFiles = mutableListOf() + + // Find files not in manifest + assetsDir.listFiles()?.forEach { file -> + if (file.isFile && !manifestFilePaths.contains(file.absolutePath)) { + orphanedFiles.add(file) + } + } + + // Delete orphaned files + orphanedFiles.forEach { file -> + println("Deleting orphaned file: ${file.name}") + file.delete() + } + + println("Cleaned up ${orphanedFiles.size} orphaned files") + orphanedFiles.size + } + } +} + +private data class Asset( + val filePath: String, + val hash: String +) + +private data class Manifest( + val assets: MutableMap = mutableMapOf() +) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt new file mode 100644 index 000000000..82cf63428 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt @@ -0,0 +1,144 @@ +package io.snabble.sdk.assetservice.data.local.image + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.security.MessageDigest + +interface LocalDiskDataSource { + + suspend fun getBitmap(key: String): Bitmap? + suspend fun saveToDisk(key: String, bitmap: Bitmap): Any + suspend fun clearCache() +} + +class LocalDiskDataSourceImpl( + private val storageDirectory: File, +) : LocalDiskDataSource { + + private val diskCacheDir: File + private val cacheScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + diskCacheDir = initDiskCacheDir() + } + + private fun initDiskCacheDir(): File { + val cacheDir = File(storageDirectory, DISK_CACHE_SUBDIR) + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + Logger.d("Init disk cache dir: ${cacheDir.absolutePath}") + + // Clean up old cache files if over size limit + cacheScope.launch { + cleanupDiskCache() + } + + return cacheDir + } + + override suspend fun getBitmap(key: String): Bitmap? = withContext(Dispatchers.IO) { + val cacheFile = getCacheFile(key) + + return@withContext if (cacheFile.exists()) { + try { + FileInputStream(cacheFile).use { inputStream -> + val bitmap = BitmapFactory.decodeStream(inputStream) + // Update file access time for LRU cleanup + cacheFile.setLastModified(System.currentTimeMillis()) + bitmap + } + } catch (e: IOException) { + Logger.e("Error reading image from disk cache: $key", e) + // Delete corrupted file + cacheFile.delete() + null + } + } else { + null + } + } + + private fun getCacheFile(key: String): File { + val safeFileName = key.toMD5() + CACHE_FILE_EXTENSION + return File(diskCacheDir, safeFileName) + } + + override suspend fun saveToDisk(key: String, bitmap: Bitmap) = withContext(Dispatchers.IO) { + val cacheFile = getCacheFile(key) + + try { + FileOutputStream(cacheFile).use { outputStream -> + if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) { + Logger.d("Saved image to disk: $key") + } else { + cacheFile.delete() + } + } + } catch (e: IOException) { + Logger.e("Error writing image to disk cache: $key", e) + cacheFile.delete() + } + } + + override suspend fun clearCache() { + try { + diskCacheDir.listFiles()?.forEach { it.delete() } + } catch (e: IOException) { + Logger.e("Error clearing disk cache", e) + } + } + + private suspend fun cleanupDiskCache() = withContext(Dispatchers.IO) { + try { + val files = diskCacheDir.listFiles() ?: return@withContext + val totalSize = files.sumOf { it.length() } + + if (totalSize > MAX_DISK_CACHE_SIZE) { + Logger.d("Cleaning disk cache: ${totalSize / (1024 * 1024)}MB") + + // Sort by last modified (oldest first) + val sortedFiles = files.sortedBy { it.lastModified() } + var currentSize = totalSize + + for (file in sortedFiles) { + if (currentSize <= MAX_DISK_CACHE_SIZE * 0.8) break // Keep 80% of max size + + currentSize -= file.length() + file.delete() + Logger.d("Deleted old cache file: ${file.name}") + } + } + } catch (e: Exception) { + Logger.e("Error during cache cleanup", e) + } + } + + private fun String.toMD5(): String { + return try { + val digest = MessageDigest.getInstance("MD5") + digest.update(this.toByteArray()) + digest.digest().joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + this.hashCode().toString() + } + } + + companion object { + + // Disk cache configuration + private const val DISK_CACHE_SUBDIR = "assets/" + private const val MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024L // 50MB + private const val CACHE_FILE_EXTENSION = ".cache" + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt new file mode 100644 index 000000000..ee2bb0f79 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt @@ -0,0 +1,89 @@ +package io.snabble.sdk.assetservice.data.local.image + +import android.graphics.Bitmap +import android.util.LruCache +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext + +interface LocalMemoryDataSource { + + val evictedItems: Flow?> + suspend fun getBitmap(key: String): Bitmap? + suspend fun putBitmap(key: String, bitmap: Bitmap) + + fun clearCache() +} + +class LocalMemorySourceImpl : LocalMemoryDataSource { + + private val memoryCache: LruCache + + private val _evictedItems: MutableStateFlow?> = MutableStateFlow(null) + override val evictedItems: StateFlow?> = _evictedItems.asStateFlow() + + init { + memoryCache = initMemoryCache() + } + + private fun initMemoryCache(): LruCache { + val cacheSize = calculateMemoryCacheSize() + Logger.d("Setup memory cache ${cacheSize / KB_TO_MB} MB") + + return object : LruCache(cacheSize) { + override fun sizeOf(key: String, bitmap: Bitmap): Int { + return bitmap.byteCount / BYTES_TO_KB + } + + override fun entryRemoved( + evicted: Boolean, + key: String, + oldValue: Bitmap, + newValue: Bitmap? + ) { + // When evicted from memory, save to disk asynchronously + if (evicted) { + _evictedItems.update { key to oldValue } + } + } + } + } + + private fun calculateMemoryCacheSize(): Int { + val maxMemoryKB = (Runtime.getRuntime().maxMemory() / BYTES_TO_KB).toInt() + val calculatedCacheKB = maxMemoryKB / MEMORY_CACHE_FRACTION + val minCacheKB = MIN_CACHE_SIZE_MB * KB_TO_MB + val maxCacheKB = MAX_CACHE_SIZE_MB * KB_TO_MB + return calculatedCacheKB.coerceIn(minCacheKB, maxCacheKB) + } + + override suspend fun getBitmap(key: String): Bitmap? = withContext(Dispatchers.IO) { + memoryCache.get(key)?.let { bitmap -> + Logger.d("Load image from memory: $key") + return@withContext bitmap + } + } + + override suspend fun putBitmap(key: String, bitmap: Bitmap) = withContext(Dispatchers.IO) { + memoryCache.put(key, bitmap) + Logger.d("Saving image to memory: $key") + } + + override fun clearCache() { + memoryCache.evictAll() + } + + companion object { + + private const val BYTES_TO_KB = 1024 + private const val KB_TO_MB = 1024 + private const val MEMORY_CACHE_FRACTION = 8 // Use 1/8 of available heap + private const val MIN_CACHE_SIZE_MB = 4 + private const val MAX_CACHE_SIZE_MB = 64 + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt index 6eef64125..c5aab356e 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt @@ -23,23 +23,34 @@ import okhttp3.Request import okhttp3.Response import okio.IOException import org.apache.commons.io.FilenameUtils +import java.security.MessageDigest import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds interface RemoteAssetsSource { - suspend fun downloadManifestForProject(project: Project): ManifestDto? + /** + * Downloads the manifest containing metadata info for the Assets (e.g. name of the assets and the variant) + */ + suspend fun downloadManifestForProject(): ManifestDto? - suspend fun downloadAllAssets(project: Project, files: List): List + /** + * Downloads the assets (e.g the bytes) for each asset variant provided. + * The asset needs to be a valid format (.svg, .jpg, .webp) and not has to be in the root folder + */ + suspend fun downloadAllAssets(files: List): List } -class RemoteAssetsSourceImpl : RemoteAssetsSource { +class RemoteAssetsSourceImpl( + private val project: Project +) : RemoteAssetsSource { - override suspend fun downloadManifestForProject(project: Project): ManifestDto? = + override suspend fun downloadManifestForProject(): ManifestDto? = with(Dispatchers.IO) { suspendCancellableCoroutine { continuation: Continuation -> val assetsUrl = project.assetsUrl ?: return@suspendCancellableCoroutine + Logger.d("Loading to Manifest from: $assetsUrl") val request = Request.Builder() .cacheControl(CacheControl.Builder().maxAge(30.seconds).build()) @@ -47,6 +58,7 @@ class RemoteAssetsSourceImpl : RemoteAssetsSource { .get() .build() + project.okHttpClient.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Logger.e(e.message) @@ -56,14 +68,16 @@ class RemoteAssetsSourceImpl : RemoteAssetsSource { override fun onResponse(call: Call, response: Response) { try { if (!response.isSuccessful) { + Logger.d("Loading manifest failed: ${response.code} ${response.body.string()}") continuation.resume(null) return } val manifestDto = GsonHolder.get().fromJson(response.body.string(), ManifestDto::class.java) + Logger.d("Loading manifest succeeded: $manifestDto") continuation.resume(manifestDto) } catch (e: JsonSyntaxException) { - Logger.e("Manifest download failed: ${e.message}") + Logger.e("Manifest parsing failed: ${e.message}") continuation.resume(null) } finally { response.close() @@ -74,72 +88,92 @@ class RemoteAssetsSourceImpl : RemoteAssetsSource { } } - // Todo: filter if the existing assets out override suspend fun downloadAllAssets( - project: Project, files: List ) = withContext(Dispatchers.IO) { val assetsUrls = files.mapNotNull { asset -> - val url: String? = asset.variants[VariantDto.MDPI] - val format = FilenameUtils.getExtension(asset.name) + val url: String = asset.variants[VariantDto.MDPI] ?: return@mapNotNull null + val assetsName: String = asset.name + val format = FilenameUtils.getExtension(assetsName) when { - format.isValidFormat() && asset.name.isRootAsset() -> url + format.isValidFormat() && asset.name.isRootAsset() -> assetsName to url else -> null } } + Logger.e("Filtered valid assets: $assetsUrls") + val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) - assetsUrls.map { url -> - async { - semaphore.withPermit { - loadAsset(project, url) + assetsUrls + .map { (assetName, url) -> + async { + semaphore.withPermit { + loadAsset(project, url, assetName) + } } - } - }.awaitAll() + }.awaitAll() + .filterNotNull() } - private suspend fun loadAsset(project: Project, url: String): AssetDto? = withContext(Dispatchers.IO) { - suspendCancellableCoroutine { continuation -> - val request = Request.Builder() - .url(Snabble.absoluteUrl(url)) - .get() - .build() - - val call = project.okHttpClient.newCall(request) + private suspend fun loadAsset(project: Project, url: String, assetName: String): AssetDto? = + withContext(Dispatchers.IO) { + suspendCancellableCoroutine { continuation -> + Logger.e("Loading asset for $url") + val request = Request.Builder() + .url(Snabble.absoluteUrl(url)) + .get() + .build() - // Handle cancellation - continuation.invokeOnCancellation { - call.cancel() - } + val call = project.okHttpClient.newCall(request) - call.enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - continuation.resume(null) + // Handle cancellation + continuation.invokeOnCancellation { + call.cancel() } - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Logger.e("Loading asset failed: ${e.message}") continuation.resume(null) - return } - val bytes = response.body.bytes() - - val asset = AssetDto(data = bytes) - - continuation.resume(asset) - response.close() - } - }) + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + Logger.e("Loading asset failed: ${response.code} ${response.body.string()}") + continuation.resume(null) + return + } + val bytes = response.body.bytes() + val asset = + AssetDto(data = bytes.inputStream(), hash = url.calculateHash(), name = assetName) + Logger.d("Loading assets succeeded: $asset") + continuation.resume(asset) + response.close() + } + }) + } } - } private fun String.isValidFormat() = VALID_FORMATS.contains(this) private fun String.isRootAsset() = !contains("/") + /** + * Calculate SHA-256 hash for a String + */ + fun String.calculateHash(): String { + return try { + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(this.toByteArray(Charsets.UTF_8)) + hashBytes.joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + Logger.e("Failed to calculate hash for string: ${e.message}", e) + throw e + } + } + companion object { private val VALID_FORMATS = listOf("svg", "jpg", "webp") diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt new file mode 100644 index 000000000..de37756e8 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt @@ -0,0 +1,12 @@ +package io.snabble.sdk.assetservice.domain + +import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.domain.model.Type +import io.snabble.sdk.assetservice.domain.model.UiMode + +interface AssetsRepository { + + suspend fun updateAllAssets() + + suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt new file mode 100644 index 000000000..c3f1c3513 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt @@ -0,0 +1,9 @@ +package io.snabble.sdk.assetservice.domain + +import android.graphics.Bitmap + +interface ImageRepository { + + suspend fun getBitmap(key: String): Bitmap? + suspend fun putBitmap(key: String, bitmap: Bitmap) +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt b/core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt new file mode 100644 index 000000000..3bc0b2da2 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt @@ -0,0 +1,11 @@ +package io.snabble.sdk.assetservice.domain.model + +/** + * Enum class for describing the image type + */ +enum class Type(val value: String) { + + SVG(".svg"), + JPG(".jpg"), + WEBP(".webp") +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt b/core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt new file mode 100644 index 000000000..14dbf985c --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt @@ -0,0 +1,7 @@ +package io.snabble.sdk.assetservice.domain.model + +enum class UiMode(val value: String) { + + NIGHT("dark"), + DAY(""), +} From dcf061cc8f8af7e55d8a50636ce1b819588fce67 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Tue, 26 Aug 2025 13:52:05 +0200 Subject: [PATCH 3/8] WIP: rewrite asset service --- .../sdk/assetservice/data/dto/AssetDto.kt | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt index 7b6214bb3..588066381 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt @@ -1,21 +1,9 @@ package io.snabble.sdk.assetservice.data.dto -data class AssetDto( - val data: ByteArray, -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AssetDto +import java.io.InputStream - if (!data.contentEquals(other.data)) return false - - return true - } - - override fun hashCode(): Int { - return data.contentHashCode() - } -} +data class AssetDto( + val name: String, + val hash: String, + val data: InputStream, +) From 8f0eb3dc52f1af8457ef1840d4bd84a2e69d1fe2 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 27 Aug 2025 16:39:39 +0200 Subject: [PATCH 4/8] clean up asset package and provide logs for debugging --- .../snabble/sdk/assetservice/AssetService.kt | 10 +- .../assets/data/AssetsRepositoryImpl.kt | 58 +++++ .../data/source/LocalAssetDataSource.kt | 176 +++++++++++++++ .../data/source}/RemoteAssetsSource.kt | 14 +- .../data/source}/dto/AssetDto.kt | 2 +- .../data/source}/dto/AssetVariantDto.kt | 2 +- .../data/source}/dto/ManifestDto.kt | 2 +- .../data/source}/dto/VariantDto.kt | 2 +- .../{ => assets}/domain/AssetsRepository.kt | 4 +- .../assetservice/data/AssetsRepositoryImpl.kt | 38 ---- .../data/local/LocalAssetDataSource.kt | 209 ------------------ 11 files changed, 252 insertions(+), 265 deletions(-) create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt rename core/src/main/java/io/snabble/sdk/assetservice/{data/remote => assets/data/source}/RemoteAssetsSource.kt (93%) rename core/src/main/java/io/snabble/sdk/assetservice/{data => assets/data/source}/dto/AssetDto.kt (67%) rename core/src/main/java/io/snabble/sdk/assetservice/{data => assets/data/source}/dto/AssetVariantDto.kt (79%) rename core/src/main/java/io/snabble/sdk/assetservice/{data => assets/data/source}/dto/ManifestDto.kt (70%) rename core/src/main/java/io/snabble/sdk/assetservice/{data => assets/data/source}/dto/VariantDto.kt (88%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => assets}/domain/AssetsRepository.kt (68%) delete mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt delete mode 100644 core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt diff --git a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt index d9a5c9604..285605e51 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt @@ -8,14 +8,14 @@ import android.graphics.Canvas import android.util.DisplayMetrics import com.caverock.androidsvg.SVG import io.snabble.sdk.Project -import io.snabble.sdk.assetservice.data.AssetsRepositoryImpl +import io.snabble.sdk.assetservice.assets.data.AssetsRepositoryImpl import io.snabble.sdk.assetservice.data.ImageRepositoryImpl -import io.snabble.sdk.assetservice.data.dto.AssetDto -import io.snabble.sdk.assetservice.data.local.LocalAssetDataSourceImpl +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSourceImpl import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSourceImpl import io.snabble.sdk.assetservice.data.local.image.LocalMemorySourceImpl -import io.snabble.sdk.assetservice.data.remote.RemoteAssetsSourceImpl -import io.snabble.sdk.assetservice.domain.AssetsRepository +import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl +import io.snabble.sdk.assetservice.assets.domain.AssetsRepository import io.snabble.sdk.assetservice.domain.ImageRepository import io.snabble.sdk.assetservice.domain.model.Type import io.snabble.sdk.assetservice.domain.model.UiMode diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt new file mode 100644 index 000000000..a996b0674 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt @@ -0,0 +1,58 @@ +package io.snabble.sdk.assetservice.assets.data + +import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSource +import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSource +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.assetservice.assets.data.source.dto.ManifestDto +import io.snabble.sdk.assetservice.assets.domain.AssetsRepository +import io.snabble.sdk.assetservice.domain.model.Type +import io.snabble.sdk.assetservice.domain.model.UiMode +import io.snabble.sdk.utils.Logger +import org.apache.commons.io.FilenameUtils + +class AssetsRepositoryImpl( + private val remoteAssetsSource: RemoteAssetsSource, + private val localAssetDataSource: LocalAssetDataSource +) : AssetsRepository { + + override suspend fun updateAllAssets() { + Logger.d("Start updating all assets. Loading manifest...") + val manifest: ManifestDto = loadManifest() ?: return + + removeDeletedAssets(manifest) + + Logger.d("Clean up orphaned files...") + localAssetDataSource.cleanupOrphanedFiles() + + val newAssets = manifest.files.filterNot { localAssetDataSource.assetExists(it.name) } + Logger.d("Filtered new assets $newAssets") + + Logger.d("Continue with loading all new assets...") + val assets: List = remoteAssetsSource.downloadAllAssets(newAssets) + + Logger.d("Saving new assets $assets locally...") + localAssetDataSource.saveMultipleAssets(assets = assets) + + } + + private suspend fun removeDeletedAssets(manifest: ManifestDto) { + val remoteAssetNames: Set = manifest.files.map { it.name }.toSet() + val deadAssets: List = localAssetDataSource.listAssets().filterNot { it in remoteAssetNames } + Logger.d("Removing deleted assets $deadAssets...") + localAssetDataSource.deleteAsset(deadAssets) + } + + private suspend fun loadManifest(): ManifestDto? = remoteAssetsSource.downloadManifest().also { + if (it == null) Logger.e("Manifest couldn't be loaded") + } + + override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? = + getLocalAsset(filename = name.createFileName(type, uiMode)) + + private suspend fun getLocalAsset(filename: String): AssetDto? = localAssetDataSource.loadAsset(filename) + + private fun String.createFileName(type: Type, uiMode: UiMode): String { + val cleanedName = FilenameUtils.removeExtension(this) + return "$cleanedName${uiMode.value}${type.value}" + } +} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt new file mode 100644 index 000000000..076ea8df1 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt @@ -0,0 +1,176 @@ +@file:Suppress("TooGenericExceptionCaught") + +package io.snabble.sdk.assetservice.assets.data.source + +import com.google.gson.annotations.SerializedName +import io.snabble.sdk.Project +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.utils.GsonHolder +import io.snabble.sdk.utils.Logger +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +interface LocalAssetDataSource { + + suspend fun loadAsset(name: String): AssetDto? + suspend fun saveMultipleAssets(assets: List) + fun assetExists(name: String): Boolean + fun listAssets(): List + suspend fun deleteAsset(names: List) + suspend fun cleanupOrphanedFiles() +} + +class LocalAssetDataSourceImpl( + private val project: Project, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : LocalAssetDataSource { + + private val assetsDir = File(project.internalStorageDirectory, "assets/") + private val manifestFile = File(project.internalStorageDirectory, "assets_v2.json") + + private var manifest = ManifestFile() + + init { + assetsDir.mkdirs() + loadManifest() + } + + override suspend fun loadAsset(name: String): AssetDto? = withContext(dispatcher) { + try { + Logger.d("Loading asset $name...") + + return@withContext manifest.assets[name] + ?.takeIf { File(it.filePath).exists() } + ?.let { file -> + + Logger.d("Asset loaded for $name") + + AssetDto( + name = name, + data = File(file.filePath).inputStream(), + hash = file.hash + ) + }.also { + if (it == null) { + Logger.e("Asset $name not found in manifest or file does not exist") + } + } + } catch (e: Exception) { + Logger.e("Loading asset failed: ${e.message}") + return@withContext null + } + } + + override suspend fun saveMultipleAssets(assets: List) = withContext(dispatcher) { + try { + assets.forEach { assetDto -> + Logger.e("Saving asset ${assetDto.name}...") + val fileName = "${assetDto.hash}_${assetDto.name}" + val file = File(assetsDir, fileName) + file.createNewFile() + + Logger.d("Created file for ${assetDto.name}") + + + assetDto.data.use { inputStream -> + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + Logger.d("Saved asset content into a file ${file.absolutePath}") + + val assetFile = AssetFile( + filePath = file.absolutePath, + hash = assetDto.hash + ) + + withContext(Dispatchers.Main) { + manifest.assets[assetDto.name] = assetFile + } + } + + if (assets.isNotEmpty()) saveManifest() + } catch (e: Exception) { + Logger.e("Saving Assets failed: ${e.message}") + } + } + + private suspend fun saveManifest() = withContext(dispatcher) { + try { + val jsonString = GsonHolder.get().toJson(manifest) + manifestFile.writeText(jsonString) + Logger.d("Saved manifest for project ${project.id}") + } catch (e: Exception) { + Logger.d("Could not save manifest: ${e.message}") + throw e + } + } + + private fun loadManifest() { + try { + if (manifestFile.exists()) { + val jsonString = manifestFile.readText() + manifest = GsonHolder.get().fromJson(jsonString, ManifestFile::class.java) + Logger.d("Loaded manifest for project ${project.id} with ${manifest.assets.size} assets") + } + Logger.e("Manifest does not exist, creating new one") + manifest = ManifestFile() + } catch (e: Exception) { + Logger.e("Could not load manifest, creating new one: ${e.message}") + manifest = ManifestFile() + } + } + + override fun listAssets(): List = manifest.assets.keys.toList() + + override fun assetExists(name: String): Boolean = manifest.assets.contains(name) + + override suspend fun deleteAsset(names: List) = withContext(dispatcher) { + try { + Logger.e("Start deleting dead assets...") + names.forEach { name -> + val asset = manifest.assets[name] ?: return@forEach + + val file = File(asset.filePath) + val deleted = if (file.exists()) file.delete() else true + + if (deleted) { + manifest.assets.remove(name) + saveManifest() + } + } + Logger.e("Deleted dead assets") + } catch (e: Exception) { + Logger.e("Deletion of dead assets failed: ${e.message}") + } + } + + override suspend fun cleanupOrphanedFiles() = withContext(dispatcher) { + try { + val files = assetsDir.listFiles() ?: return@withContext + val manifestFilePaths = manifest.assets.values.map { it.filePath }.toSet() + + files.filterNotNull() + .filter { file -> file.isFile && !manifestFilePaths.contains(file.absolutePath) } + .map { file -> + Logger.d("Deleting orphaned file: ${file.name}") + file.delete() + } + } catch (e: Exception) { + Logger.e("Failed to delete orphaned files: ${e.message}") + } + } +} + +private data class AssetFile( + @SerializedName("filePath") val filePath: String, + @SerializedName("hash") val hash: String +) + +private data class ManifestFile( + @SerializedName("assets") val assets: MutableMap = mutableMapOf() +) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt similarity index 93% rename from core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt index c5aab356e..53eb6f4c9 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/remote/RemoteAssetsSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt @@ -1,12 +1,12 @@ -package io.snabble.sdk.assetservice.data.remote +package io.snabble.sdk.assetservice.assets.data.source import com.google.gson.JsonSyntaxException import io.snabble.sdk.Project import io.snabble.sdk.Snabble -import io.snabble.sdk.assetservice.data.dto.AssetDto -import io.snabble.sdk.assetservice.data.dto.AssetVariantDto -import io.snabble.sdk.assetservice.data.dto.ManifestDto -import io.snabble.sdk.assetservice.data.dto.VariantDto +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetVariantDto +import io.snabble.sdk.assetservice.assets.data.source.dto.ManifestDto +import io.snabble.sdk.assetservice.assets.data.source.dto.VariantDto import io.snabble.sdk.utils.GsonHolder import io.snabble.sdk.utils.Logger import kotlinx.coroutines.Dispatchers @@ -33,7 +33,7 @@ interface RemoteAssetsSource { /** * Downloads the manifest containing metadata info for the Assets (e.g. name of the assets and the variant) */ - suspend fun downloadManifestForProject(): ManifestDto? + suspend fun downloadManifest(): ManifestDto? /** * Downloads the assets (e.g the bytes) for each asset variant provided. @@ -46,7 +46,7 @@ class RemoteAssetsSourceImpl( private val project: Project ) : RemoteAssetsSource { - override suspend fun downloadManifestForProject(): ManifestDto? = + override suspend fun downloadManifest(): ManifestDto? = with(Dispatchers.IO) { suspendCancellableCoroutine { continuation: Continuation -> val assetsUrl = project.assetsUrl ?: return@suspendCancellableCoroutine diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt similarity index 67% rename from core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt index 588066381..5b2a8e909 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.dto +package io.snabble.sdk.assetservice.assets.data.source.dto import java.io.InputStream diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt similarity index 79% rename from core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt index fd794e165..b534b0adc 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/AssetVariantDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.dto +package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt similarity index 70% rename from core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt index e038fb7fc..8153a65ae 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/ManifestDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.dto +package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt similarity index 88% rename from core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt index a647c0be7..1a7782bfd 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/dto/VariantDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.dto +package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt similarity index 68% rename from core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt rename to core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt index de37756e8..8c9eb14ce 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/domain/AssetsRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt @@ -1,6 +1,6 @@ -package io.snabble.sdk.assetservice.domain +package io.snabble.sdk.assetservice.assets.domain -import io.snabble.sdk.assetservice.data.dto.AssetDto +import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto import io.snabble.sdk.assetservice.domain.model.Type import io.snabble.sdk.assetservice.domain.model.UiMode diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt deleted file mode 100644 index 52a18a443..000000000 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/AssetsRepositoryImpl.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.snabble.sdk.assetservice.data - -import io.snabble.sdk.assetservice.data.dto.AssetDto -import io.snabble.sdk.assetservice.data.dto.ManifestDto -import io.snabble.sdk.assetservice.data.local.LocalAssetDataSource -import io.snabble.sdk.assetservice.data.remote.RemoteAssetsSource -import io.snabble.sdk.assetservice.domain.AssetsRepository -import io.snabble.sdk.assetservice.domain.model.Type -import io.snabble.sdk.assetservice.domain.model.UiMode -import io.snabble.sdk.utils.Logger -import org.apache.commons.io.FilenameUtils - -class AssetsRepositoryImpl( - private val remoteAssetsSource: RemoteAssetsSource, - private val localAssetDataSource: LocalAssetDataSource -) : AssetsRepository { - - override suspend fun updateAllAssets() { - Logger.e("Start updating all assets...") - val manifest: ManifestDto = remoteAssetsSource.downloadManifestForProject() ?: return - val newAssets = localAssetDataSource.removeExistingAssets(manifest.files) - Logger.e("Filtered new assets $newAssets") - Logger.e("Continue with loading all new assets...") - val assets: List = remoteAssetsSource.downloadAllAssets(newAssets) - Logger.e("Saving new assets $assets locally...") - localAssetDataSource.saveMultipleAssets(assets = assets) - } - - override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? = - getLocalAsset(filename = name.createFileName(type, uiMode)) - - private suspend fun getLocalAsset(filename: String): AssetDto? = localAssetDataSource.loadAsset(filename) - - private fun String.createFileName(type: Type, uiMode: UiMode): String { - val cleanedName = FilenameUtils.removeExtension(this) - return "$cleanedName${uiMode.value}${type.value}" - } -} diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt deleted file mode 100644 index 9492a8769..000000000 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/local/LocalAssetDataSource.kt +++ /dev/null @@ -1,209 +0,0 @@ -package io.snabble.sdk.assetservice.data.local - -import io.snabble.sdk.Project -import io.snabble.sdk.assetservice.data.dto.AssetDto -import io.snabble.sdk.assetservice.data.dto.AssetVariantDto -import io.snabble.sdk.extensions.xx -import io.snabble.sdk.utils.GsonHolder -import io.snabble.sdk.utils.Logger -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.apache.commons.io.IOUtils -import java.io.File -import java.io.FileOutputStream - -interface LocalAssetDataSource { - - suspend fun loadAsset(name: String): AssetDto? - suspend fun saveMultipleAssets(assets: List) - suspend fun removeExistingAssets(assets: List): List -} - -class LocalAssetDataSourceImpl( - private val project: Project, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) : LocalAssetDataSource { - - private val assetsDir = File(project.internalStorageDirectory, "assets/") - private val manifestFile = File(project.internalStorageDirectory, "assets_v2.json") - - private var manifest = Manifest() - - init { - assetsDir.mkdirs() - loadManifest() - } - - override suspend fun loadAsset(name: String): AssetDto? = withContext(dispatcher) { - try { - manifest.assets.contains(name).xx("contains $name") - val asset = manifest.assets[name] - ?: throw IllegalArgumentException("Asset '$name' not found in manifest") - - val file = File(asset.filePath) - if (!file.exists()) { - throw IllegalStateException("Asset file not found: ${asset.filePath}") - } - - return@withContext AssetDto( - name = name, - data = file.inputStream(), - hash = asset.hash - ) - } catch (e: Exception) { - Logger.e(e.message) - return@withContext null - } - } - - override suspend fun saveMultipleAssets(assets: List) = withContext(dispatcher) { - try { - assets.forEach { assetDto -> - val fileName = "${assetDto.hash}_${assetDto.name}" - val file = File(assetsDir, fileName) - file.createNewFile() - // Ensure assets directory exists - if (!assetsDir.exists()) { - assetsDir.mkdirs() - } - assetDto.data.use { inputStream -> - FileOutputStream(file).use { outputStream -> - inputStream.copyTo(outputStream) - } - } - // Create asset entry - val asset = Asset( - filePath = file.absolutePath, - hash = assetDto.hash - ) - - withContext(Dispatchers.Main) { - "save asset ${assetDto.name}".xx() - manifest.assets[assetDto.name] = asset - } - } - - // Save manifest once after all assets - saveManifest() - } catch (e: Exception) { - e.xx("Fuak me ${e.cause}") - Logger.e(e.message) - } - } - - override suspend fun removeExistingAssets(assets: List): List = - assets.filterNot { manifest.assets.contains(it.name) } - - private suspend fun saveManifest() = withContext(dispatcher) { - try { - val jsonString = GsonHolder.get().toJson(manifest) - manifestFile.writeText(jsonString) - println("Saved manifest for project ${project.id}") - } catch (e: Exception) { - println("Could not write manifest: ${e.message}") - throw e - } - } - - private fun loadManifest() { - try { - if (manifestFile.exists()) { - val jsonString = manifestFile.readText() - manifest = GsonHolder.get().fromJson(jsonString, Manifest::class.java) - println("Loaded manifest for project ${project.id} with ${manifest.assets.size} assets") - } - } catch (e: Exception) { - println("Could not load manifest, creating new one: ${e.message}") - manifest = Manifest() - } - } - - fun listAssets(): List = manifest.assets.keys.toList() - - fun assetExists(name: String): Boolean = manifest.assets.containsKey(name) - - suspend fun deleteAsset(name: String): Result = withContext(dispatcher) { - runCatching { - val asset = manifest.assets[name] ?: return@runCatching false - - val file = File(asset.filePath) - val deleted = if (file.exists()) file.delete() else true - - if (deleted) { - manifest.assets.remove(name) - saveManifest() - } - - deleted - } - } - - suspend fun cleanupUnusedAssets(referencedHashes: Set): Result = withContext(dispatcher) { - runCatching { - val removals = mutableListOf() - var hasChanges = false - - // Find assets to remove - manifest.assets.forEach { (name, asset) -> - if (!referencedHashes.contains(asset.hash)) { - println("Removing unused asset: $name") - - // Delete file - val file = File(asset.filePath) - if (file.exists()) { - file.delete() - } - - removals.add(name) - hasChanges = true - } - } - - // Remove from manifest - removals.forEach { name -> - manifest.assets.remove(name) - } - - // Save manifest if changes were made - if (hasChanges) { - saveManifest() - println("Cleaned up ${removals.size} unused assets for project ${project.id}") - } - - removals.size - } - } - - suspend fun cleanupOrphanedFiles(): Result = withContext(dispatcher) { - runCatching { - val manifestFilePaths = manifest.assets.values.map { it.filePath }.toSet() - val orphanedFiles = mutableListOf() - - // Find files not in manifest - assetsDir.listFiles()?.forEach { file -> - if (file.isFile && !manifestFilePaths.contains(file.absolutePath)) { - orphanedFiles.add(file) - } - } - - // Delete orphaned files - orphanedFiles.forEach { file -> - println("Deleting orphaned file: ${file.name}") - file.delete() - } - - println("Cleaned up ${orphanedFiles.size} orphaned files") - orphanedFiles.size - } - } -} - -private data class Asset( - val filePath: String, - val hash: String -) - -private data class Manifest( - val assets: MutableMap = mutableMapOf() -) From d28529de0c3e21b9eb73b2586dff607d6d952652 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 27 Aug 2025 16:43:09 +0200 Subject: [PATCH 5/8] =?UTF-8?q?provide=20model=20instead=20of=20dto=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/snabble/sdk/assetservice/AssetService.kt | 5 +++-- .../sdk/assetservice/assets/data/AssetsRepositoryImpl.kt | 8 +++++--- .../assets/data/source/RemoteAssetsSource.kt | 4 ++-- .../sdk/assetservice/assets/domain/AssetsRepository.kt | 3 ++- .../sdk/assetservice/assets/domain/model/Asset.kt | 9 +++++++++ 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt diff --git a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt index 285605e51..799a1ded0 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt @@ -16,6 +16,7 @@ import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSourceImpl import io.snabble.sdk.assetservice.data.local.image.LocalMemorySourceImpl import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl import io.snabble.sdk.assetservice.assets.domain.AssetsRepository +import io.snabble.sdk.assetservice.assets.domain.model.Asset import io.snabble.sdk.assetservice.domain.ImageRepository import io.snabble.sdk.assetservice.domain.model.Type import io.snabble.sdk.assetservice.domain.model.UiMode @@ -81,7 +82,7 @@ class AssetServiceImpl( private suspend fun createBitmap(name: String, type: Type, uiMode: UiMode): Bitmap? { "create Bitmap" val cachedAsset = - assetRepository.loadAsset(name = name, type = type, uiMode = uiMode).xx("loaded Asset") ?: return null + assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) ?: return null return when (type) { Type.SVG -> createSVGBitmap(cachedAsset.data) Type.JPG, @@ -89,7 +90,7 @@ class AssetServiceImpl( } } - private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): AssetDto? { + private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): Asset? { assetRepository.updateAllAssets() return assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) } diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt index a996b0674..09aebc849 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt @@ -5,6 +5,7 @@ import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSource import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto import io.snabble.sdk.assetservice.assets.data.source.dto.ManifestDto import io.snabble.sdk.assetservice.assets.domain.AssetsRepository +import io.snabble.sdk.assetservice.assets.domain.model.Asset import io.snabble.sdk.assetservice.domain.model.Type import io.snabble.sdk.assetservice.domain.model.UiMode import io.snabble.sdk.utils.Logger @@ -32,7 +33,6 @@ class AssetsRepositoryImpl( Logger.d("Saving new assets $assets locally...") localAssetDataSource.saveMultipleAssets(assets = assets) - } private suspend fun removeDeletedAssets(manifest: ManifestDto) { @@ -46,8 +46,8 @@ class AssetsRepositoryImpl( if (it == null) Logger.e("Manifest couldn't be loaded") } - override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? = - getLocalAsset(filename = name.createFileName(type, uiMode)) + override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Asset? = + getLocalAsset(filename = name.createFileName(type, uiMode))?.toModel() private suspend fun getLocalAsset(filename: String): AssetDto? = localAssetDataSource.loadAsset(filename) @@ -56,3 +56,5 @@ class AssetsRepositoryImpl( return "$cleanedName${uiMode.value}${type.value}" } } + +private fun AssetDto.toModel() = Asset(name = name, hash = hash, data = data) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt index 53eb6f4c9..2144040fb 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt @@ -102,7 +102,7 @@ class RemoteAssetsSourceImpl( } } - Logger.e("Filtered valid assets: $assetsUrls") + Logger.d("Filtered valid assets: $assetsUrls") val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) @@ -120,7 +120,7 @@ class RemoteAssetsSourceImpl( private suspend fun loadAsset(project: Project, url: String, assetName: String): AssetDto? = withContext(Dispatchers.IO) { suspendCancellableCoroutine { continuation -> - Logger.e("Loading asset for $url") + Logger.d("Loading asset for $url") val request = Request.Builder() .url(Snabble.absoluteUrl(url)) .get() diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt index 8c9eb14ce..ace009195 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt @@ -1,6 +1,7 @@ package io.snabble.sdk.assetservice.assets.domain import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.assetservice.assets.domain.model.Asset import io.snabble.sdk.assetservice.domain.model.Type import io.snabble.sdk.assetservice.domain.model.UiMode @@ -8,5 +9,5 @@ interface AssetsRepository { suspend fun updateAllAssets() - suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): AssetDto? + suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Asset? } diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt new file mode 100644 index 000000000..eb9cf72b8 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt @@ -0,0 +1,9 @@ +package io.snabble.sdk.assetservice.assets.domain.model + +import java.io.InputStream + +data class Asset( + val name: String, + val hash: String, + val data: InputStream, +) From 876fb8cd31aea0505760b69a89911118084fd43c Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Wed, 27 Aug 2025 16:52:21 +0200 Subject: [PATCH 6/8] move to different package --- .../io/snabble/sdk/assetservice/AssetService.kt | 14 ++++++-------- .../assets/data/AssetsRepositoryImpl.kt | 4 ++-- .../assetservice/assets/domain/AssetsRepository.kt | 5 ++--- .../{ => image}/data/ImageRepositoryImpl.kt | 8 ++++---- .../data/local/image/LocalDiskDataSource.kt | 2 +- .../data/local/image/LocalMemoryDataSource.kt | 2 +- .../{ => image}/domain/ImageRepository.kt | 2 +- .../assetservice/{ => image}/domain/model/Type.kt | 2 +- .../{ => image}/domain/model/UiMode.kt | 2 +- 9 files changed, 19 insertions(+), 22 deletions(-) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/data/ImageRepositoryImpl.kt (89%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/data/local/image/LocalDiskDataSource.kt (98%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/data/local/image/LocalMemoryDataSource.kt (98%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/domain/ImageRepository.kt (77%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/domain/model/Type.kt (72%) rename core/src/main/java/io/snabble/sdk/assetservice/{ => image}/domain/model/UiMode.kt (57%) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt index 799a1ded0..544bc13f1 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt @@ -9,18 +9,16 @@ import android.util.DisplayMetrics import com.caverock.androidsvg.SVG import io.snabble.sdk.Project import io.snabble.sdk.assetservice.assets.data.AssetsRepositoryImpl -import io.snabble.sdk.assetservice.data.ImageRepositoryImpl -import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto +import io.snabble.sdk.assetservice.image.data.ImageRepositoryImpl import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSourceImpl -import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSourceImpl -import io.snabble.sdk.assetservice.data.local.image.LocalMemorySourceImpl +import io.snabble.sdk.assetservice.image.data.local.image.LocalDiskDataSourceImpl +import io.snabble.sdk.assetservice.image.data.local.image.LocalMemorySourceImpl import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl import io.snabble.sdk.assetservice.assets.domain.AssetsRepository import io.snabble.sdk.assetservice.assets.domain.model.Asset -import io.snabble.sdk.assetservice.domain.ImageRepository -import io.snabble.sdk.assetservice.domain.model.Type -import io.snabble.sdk.assetservice.domain.model.UiMode -import io.snabble.sdk.extensions.xx +import io.snabble.sdk.assetservice.image.domain.ImageRepository +import io.snabble.sdk.assetservice.image.domain.model.Type +import io.snabble.sdk.assetservice.image.domain.model.UiMode import io.snabble.sdk.utils.Logger import java.io.InputStream import kotlin.math.roundToInt diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt index 09aebc849..bc678fdd1 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt @@ -6,8 +6,8 @@ import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto import io.snabble.sdk.assetservice.assets.data.source.dto.ManifestDto import io.snabble.sdk.assetservice.assets.domain.AssetsRepository import io.snabble.sdk.assetservice.assets.domain.model.Asset -import io.snabble.sdk.assetservice.domain.model.Type -import io.snabble.sdk.assetservice.domain.model.UiMode +import io.snabble.sdk.assetservice.image.domain.model.Type +import io.snabble.sdk.assetservice.image.domain.model.UiMode import io.snabble.sdk.utils.Logger import org.apache.commons.io.FilenameUtils diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt index ace009195..f1e5b26a6 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt @@ -1,9 +1,8 @@ package io.snabble.sdk.assetservice.assets.domain -import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto import io.snabble.sdk.assetservice.assets.domain.model.Asset -import io.snabble.sdk.assetservice.domain.model.Type -import io.snabble.sdk.assetservice.domain.model.UiMode +import io.snabble.sdk.assetservice.image.domain.model.Type +import io.snabble.sdk.assetservice.image.domain.model.UiMode interface AssetsRepository { diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt similarity index 89% rename from core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt index 63d0ce397..b0035b76d 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/ImageRepositoryImpl.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt @@ -1,9 +1,9 @@ -package io.snabble.sdk.assetservice.data +package io.snabble.sdk.assetservice.image.data import android.graphics.Bitmap -import io.snabble.sdk.assetservice.data.local.image.LocalDiskDataSource -import io.snabble.sdk.assetservice.data.local.image.LocalMemoryDataSource -import io.snabble.sdk.assetservice.domain.ImageRepository +import io.snabble.sdk.assetservice.image.data.local.image.LocalDiskDataSource +import io.snabble.sdk.assetservice.image.data.local.image.LocalMemoryDataSource +import io.snabble.sdk.assetservice.image.domain.ImageRepository import io.snabble.sdk.utils.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt similarity index 98% rename from core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt index 82cf63428..3d6302dc2 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalDiskDataSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.local.image +package io.snabble.sdk.assetservice.image.data.local.image import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt similarity index 98% rename from core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt index ee2bb0f79..c65e9bea5 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/data/local/image/LocalMemoryDataSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.data.local.image +package io.snabble.sdk.assetservice.image.data.local.image import android.graphics.Bitmap import android.util.LruCache diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt similarity index 77% rename from core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt index c3f1c3513..592569979 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/domain/ImageRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.domain +package io.snabble.sdk.assetservice.image.domain import android.graphics.Bitmap diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/Type.kt similarity index 72% rename from core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/Type.kt index 3bc0b2da2..2a0d04dc5 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/Type.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/Type.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.domain.model +package io.snabble.sdk.assetservice.image.domain.model /** * Enum class for describing the image type diff --git a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/UiMode.kt similarity index 57% rename from core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt rename to core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/UiMode.kt index 14dbf985c..c8de59167 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/domain/model/UiMode.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/model/UiMode.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.assetservice.domain.model +package io.snabble.sdk.assetservice.image.domain.model enum class UiMode(val value: String) { From 78683d3165ae9a1e30416b6903c22cbae9c03b6e Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Thu, 28 Aug 2025 09:22:07 +0200 Subject: [PATCH 7/8] update docs and adjust scopes --- .../snabble/sdk/assetservice/AssetService.kt | 37 +++++++++++++------ .../assets/data/AssetsRepositoryImpl.kt | 2 +- .../data/source/LocalAssetDataSource.kt | 4 +- .../assets/data/source/RemoteAssetsSource.kt | 4 +- .../assets/data/source/dto/AssetDto.kt | 2 +- .../assets/data/source/dto/AssetVariantDto.kt | 2 +- .../assets/data/source/dto/ManifestDto.kt | 2 +- .../assets/data/source/dto/VariantDto.kt | 2 +- .../assets/domain/AssetsRepository.kt | 2 +- .../assetservice/assets/domain/model/Asset.kt | 2 +- .../image/data/ImageRepositoryImpl.kt | 2 +- .../data/local/image/LocalDiskDataSource.kt | 26 +++++++++---- .../data/local/image/LocalMemoryDataSource.kt | 4 +- .../image/domain/ImageRepository.kt | 2 +- 14 files changed, 59 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt index 544bc13f1..688d59dd6 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt @@ -1,3 +1,5 @@ +@file:Suppress("TooGenericExceptionCaught") + package io.snabble.sdk.assetservice import android.content.Context @@ -9,13 +11,12 @@ import android.util.DisplayMetrics import com.caverock.androidsvg.SVG import io.snabble.sdk.Project import io.snabble.sdk.assetservice.assets.data.AssetsRepositoryImpl -import io.snabble.sdk.assetservice.image.data.ImageRepositoryImpl import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSourceImpl -import io.snabble.sdk.assetservice.image.data.local.image.LocalDiskDataSourceImpl -import io.snabble.sdk.assetservice.image.data.local.image.LocalMemorySourceImpl import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl import io.snabble.sdk.assetservice.assets.domain.AssetsRepository -import io.snabble.sdk.assetservice.assets.domain.model.Asset +import io.snabble.sdk.assetservice.image.data.ImageRepositoryImpl +import io.snabble.sdk.assetservice.image.data.local.image.LocalDiskDataSourceImpl +import io.snabble.sdk.assetservice.image.data.local.image.LocalMemorySourceImpl import io.snabble.sdk.assetservice.image.domain.ImageRepository import io.snabble.sdk.assetservice.image.domain.model.Type import io.snabble.sdk.assetservice.image.domain.model.UiMode @@ -30,24 +31,39 @@ interface AssetService { suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap? } -class AssetServiceImpl( +internal class AssetServiceImpl( private val displayMetrics: DisplayMetrics, private val assetRepository: AssetsRepository, private val imageRepository: ImageRepository, ) : AssetService { + /** + * Updates all assets and safes them locally + */ override suspend fun updateAllAssets() { assetRepository.updateAllAssets() } + /** + * Loads an asset and returns it converted as [Bitmap]. + * Bitmap type can be any of these [Type]. + * To define the [UiMode] use the helper function [Context.getUiMode] or set it directly if needed. + */ override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap? { val bitmap = when (val bitmap = imageRepository.getBitmap(key = name)) { null -> createBitmap(name, type, uiMode) else -> bitmap - } ?: return null + } - //Save converted bitmap - imageRepository.putBitmap(name, bitmap) + if (bitmap == null) { + val newBitmap = updateAssetsAndRetry(name, type, uiMode) + return newBitmap?.also { + imageRepository.putBitmap(name, it) + } + } else { + //Save converted bitmap + imageRepository.putBitmap(name, bitmap) + } return bitmap } @@ -78,7 +94,6 @@ class AssetServiceImpl( } private suspend fun createBitmap(name: String, type: Type, uiMode: UiMode): Bitmap? { - "create Bitmap" val cachedAsset = assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) ?: return null return when (type) { @@ -88,9 +103,9 @@ class AssetServiceImpl( } } - private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): Asset? { + private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): Bitmap? { assetRepository.updateAllAssets() - return assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) + return createBitmap(name, type, uiMode) } } diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt index bc678fdd1..530017c8e 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/AssetsRepositoryImpl.kt @@ -11,7 +11,7 @@ import io.snabble.sdk.assetservice.image.domain.model.UiMode import io.snabble.sdk.utils.Logger import org.apache.commons.io.FilenameUtils -class AssetsRepositoryImpl( +internal class AssetsRepositoryImpl( private val remoteAssetsSource: RemoteAssetsSource, private val localAssetDataSource: LocalAssetDataSource ) : AssetsRepository { diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt index 076ea8df1..4f97cfef9 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/LocalAssetDataSource.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream -interface LocalAssetDataSource { +internal interface LocalAssetDataSource { suspend fun loadAsset(name: String): AssetDto? suspend fun saveMultipleAssets(assets: List) @@ -23,7 +23,7 @@ interface LocalAssetDataSource { suspend fun cleanupOrphanedFiles() } -class LocalAssetDataSourceImpl( +internal class LocalAssetDataSourceImpl( private val project: Project, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : LocalAssetDataSource { diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt index 2144040fb..999e60a58 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/RemoteAssetsSource.kt @@ -28,7 +28,7 @@ import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds -interface RemoteAssetsSource { +internal interface RemoteAssetsSource { /** * Downloads the manifest containing metadata info for the Assets (e.g. name of the assets and the variant) @@ -42,7 +42,7 @@ interface RemoteAssetsSource { suspend fun downloadAllAssets(files: List): List } -class RemoteAssetsSourceImpl( +internal class RemoteAssetsSourceImpl( private val project: Project ) : RemoteAssetsSource { diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt index 5b2a8e909..1596c86c3 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetDto.kt @@ -2,7 +2,7 @@ package io.snabble.sdk.assetservice.assets.data.source.dto import java.io.InputStream -data class AssetDto( +internal data class AssetDto( val name: String, val hash: String, val data: InputStream, diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt index b534b0adc..8c12d3a99 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/AssetVariantDto.kt @@ -2,7 +2,7 @@ package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName -data class AssetVariantDto( +internal data class AssetVariantDto( @SerializedName("name") var name: String, @SerializedName("variants") var variants: MutableMap = mutableMapOf() ) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt index 8153a65ae..2110705ce 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/ManifestDto.kt @@ -2,6 +2,6 @@ package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName -data class ManifestDto( +internal data class ManifestDto( @SerializedName("files") val files: List ) diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt index 1a7782bfd..77d106f81 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/data/source/dto/VariantDto.kt @@ -2,7 +2,7 @@ package io.snabble.sdk.assetservice.assets.data.source.dto import com.google.gson.annotations.SerializedName -enum class VariantDto(var factor: String?, var density: Float) { +internal enum class VariantDto(var factor: String?, var density: Float) { @SerializedName("1x") MDPI(factor = "1x", density = 1.0f), diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt index f1e5b26a6..fc0564d22 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/AssetsRepository.kt @@ -4,7 +4,7 @@ import io.snabble.sdk.assetservice.assets.domain.model.Asset import io.snabble.sdk.assetservice.image.domain.model.Type import io.snabble.sdk.assetservice.image.domain.model.UiMode -interface AssetsRepository { +internal interface AssetsRepository { suspend fun updateAllAssets() diff --git a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt index eb9cf72b8..a18f04bd1 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/assets/domain/model/Asset.kt @@ -2,7 +2,7 @@ package io.snabble.sdk.assetservice.assets.domain.model import java.io.InputStream -data class Asset( +internal data class Asset( val name: String, val hash: String, val data: InputStream, diff --git a/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt index b0035b76d..0e15625e0 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/ImageRepositoryImpl.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ImageRepositoryImpl( +internal class ImageRepositoryImpl( private val localMemoryDataSource: LocalMemoryDataSource, private val localDiskDataSource: LocalDiskDataSource ) : ImageRepository { diff --git a/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt index 3d6302dc2..b03a54c6a 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalDiskDataSource.kt @@ -1,3 +1,5 @@ +@file:Suppress("TooGenericExceptionCaught") + package io.snabble.sdk.assetservice.image.data.local.image import android.graphics.Bitmap @@ -13,15 +15,16 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.security.MessageDigest +import java.security.NoSuchAlgorithmException -interface LocalDiskDataSource { +internal interface LocalDiskDataSource { suspend fun getBitmap(key: String): Bitmap? suspend fun saveToDisk(key: String, bitmap: Bitmap): Any suspend fun clearCache() } -class LocalDiskDataSourceImpl( +internal class LocalDiskDataSourceImpl( private val storageDirectory: File, ) : LocalDiskDataSource { @@ -79,7 +82,7 @@ class LocalDiskDataSourceImpl( try { FileOutputStream(cacheFile).use { outputStream -> - if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) { + if (bitmap.compress(Bitmap.CompressFormat.PNG, PNG_COMPRESSION_QUALITY, outputStream)) { Logger.d("Saved image to disk: $key") } else { cacheFile.delete() @@ -105,14 +108,14 @@ class LocalDiskDataSourceImpl( val totalSize = files.sumOf { it.length() } if (totalSize > MAX_DISK_CACHE_SIZE) { - Logger.d("Cleaning disk cache: ${totalSize / (1024 * 1024)}MB") + Logger.d("Cleaning disk cache: ${totalSize / (BYTES_TO_MB)}MB") // Sort by last modified (oldest first) val sortedFiles = files.sortedBy { it.lastModified() } var currentSize = totalSize for (file in sortedFiles) { - if (currentSize <= MAX_DISK_CACHE_SIZE * 0.8) break // Keep 80% of max size + if (currentSize <= MAX_DISK_CACHE_SIZE * CACHE_CLEANUP_TARGET_RATIO) break // Keep 80% of max size currentSize -= file.length() file.delete() @@ -127,10 +130,14 @@ class LocalDiskDataSourceImpl( private fun String.toMD5(): String { return try { val digest = MessageDigest.getInstance("MD5") - digest.update(this.toByteArray()) + digest.update(toByteArray()) digest.digest().joinToString("") { "%02x".format(it) } - } catch (e: Exception) { - this.hashCode().toString() + } catch (e: NoSuchAlgorithmException) { + Logger.e("MD5 algorithm not available", e) + hashCode().toString() + } catch (e: OutOfMemoryError) { + Logger.e("Out of memory creating MD5", e) + hashCode().toString() } } @@ -140,5 +147,8 @@ class LocalDiskDataSourceImpl( private const val DISK_CACHE_SUBDIR = "assets/" private const val MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024L // 50MB private const val CACHE_FILE_EXTENSION = ".cache" + private const val PNG_COMPRESSION_QUALITY = 100 + private const val BYTES_TO_MB = 1024 * 1024 + private const val CACHE_CLEANUP_TARGET_RATIO = 0.8 } } diff --git a/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt index c65e9bea5..2e03f5f9b 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/data/local/image/LocalMemoryDataSource.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -interface LocalMemoryDataSource { +internal interface LocalMemoryDataSource { val evictedItems: Flow?> suspend fun getBitmap(key: String): Bitmap? @@ -20,7 +20,7 @@ interface LocalMemoryDataSource { fun clearCache() } -class LocalMemorySourceImpl : LocalMemoryDataSource { +internal class LocalMemorySourceImpl : LocalMemoryDataSource { private val memoryCache: LruCache diff --git a/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt index 592569979..c06c736ea 100644 --- a/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt +++ b/core/src/main/java/io/snabble/sdk/assetservice/image/domain/ImageRepository.kt @@ -2,7 +2,7 @@ package io.snabble.sdk.assetservice.image.domain import android.graphics.Bitmap -interface ImageRepository { +internal interface ImageRepository { suspend fun getBitmap(key: String): Bitmap? suspend fun putBitmap(key: String, bitmap: Bitmap) From 6d1c1cfaf61fe78e4ca6ef149506c8a2709444e0 Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Thu, 28 Aug 2025 09:25:08 +0200 Subject: [PATCH 8/8] remove Logging interceptor --- core/src/main/java/io/snabble/sdk/Project.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/Project.kt b/core/src/main/java/io/snabble/sdk/Project.kt index 833bc3c20..bdfa35d6b 100644 --- a/core/src/main/java/io/snabble/sdk/Project.kt +++ b/core/src/main/java/io/snabble/sdk/Project.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.logging.HttpLoggingInterceptor import org.apache.commons.lang3.LocaleUtils import java.io.File import java.math.RoundingMode @@ -554,7 +553,6 @@ class Project internal constructor( .newBuilder() .addInterceptor(SnabbleAuthorizationInterceptor(this)) .addInterceptor(AcceptedLanguageInterceptor()) - .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() _shoppingCart.tryEmit(ShoppingCart(this))