Skip to content

Commit c4b50f3

Browse files
authored
Improve assets service (Apps-2456) (#260)
1 parent b5c0152 commit c4b50f3

File tree

17 files changed

+987
-1
lines changed

17 files changed

+987
-1
lines changed

core/src/main/java/io/snabble/sdk/Project.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import com.google.gson.JsonElement
55
import com.google.gson.JsonObject
66
import com.google.gson.JsonSyntaxException
77
import com.google.gson.reflect.TypeToken
8+
import io.snabble.sdk.assetservice.AssetService
9+
import io.snabble.sdk.assetservice.assetServiceFactory
810
import io.snabble.sdk.auth.SnabbleAuthorizationInterceptor
911
import io.snabble.sdk.checkout.Checkout
1012
import io.snabble.sdk.codes.templates.CodeTemplate
@@ -357,6 +359,9 @@ class Project internal constructor(
357359
lateinit var assets: Assets
358360
private set
359361

362+
lateinit var assetService: AssetService
363+
private set
364+
360365
var appTheme: AppTheme? = null
361366
private set
362367

@@ -567,6 +572,8 @@ class Project internal constructor(
567572

568573
assets = Assets(this)
569574

575+
assetService = assetServiceFactory(project = this, context = Snabble.application)
576+
570577
googlePayHelper = paymentMethodDescriptors
571578
.mapNotNull { it.paymentMethod }
572579
.firstOrNull { it == PaymentMethod.GOOGLE_PAY }
@@ -579,7 +586,6 @@ class Project internal constructor(
579586
coupons.setProjectCoupons(couponList)
580587
}
581588
coupons.update()
582-
583589
notifyUpdate()
584590
}
585591

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
@file:Suppress("TooGenericExceptionCaught")
2+
3+
package io.snabble.sdk.assetservice
4+
5+
import android.content.Context
6+
import android.content.res.Configuration
7+
import android.graphics.Bitmap
8+
import android.graphics.BitmapFactory
9+
import android.graphics.Canvas
10+
import android.util.DisplayMetrics
11+
import com.caverock.androidsvg.SVG
12+
import io.snabble.sdk.Project
13+
import io.snabble.sdk.assetservice.assets.data.AssetsRepositoryImpl
14+
import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSourceImpl
15+
import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl
16+
import io.snabble.sdk.assetservice.assets.domain.AssetsRepository
17+
import io.snabble.sdk.assetservice.image.data.ImageRepositoryImpl
18+
import io.snabble.sdk.assetservice.image.data.local.image.LocalDiskDataSourceImpl
19+
import io.snabble.sdk.assetservice.image.data.local.image.LocalMemorySourceImpl
20+
import io.snabble.sdk.assetservice.image.domain.ImageRepository
21+
import io.snabble.sdk.assetservice.image.domain.model.Type
22+
import io.snabble.sdk.assetservice.image.domain.model.UiMode
23+
import io.snabble.sdk.utils.Logger
24+
import java.io.InputStream
25+
import kotlin.math.roundToInt
26+
27+
interface AssetService {
28+
29+
suspend fun updateAllAssets()
30+
31+
suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap?
32+
}
33+
34+
internal class AssetServiceImpl(
35+
private val displayMetrics: DisplayMetrics,
36+
private val assetRepository: AssetsRepository,
37+
private val imageRepository: ImageRepository,
38+
) : AssetService {
39+
40+
/**
41+
* Updates all assets and safes them locally
42+
*/
43+
override suspend fun updateAllAssets() {
44+
assetRepository.updateAllAssets()
45+
}
46+
47+
/**
48+
* Loads an asset and returns it converted as [Bitmap].
49+
* Bitmap type can be any of these [Type].
50+
* To define the [UiMode] use the helper function [Context.getUiMode] or set it directly if needed.
51+
*/
52+
override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Bitmap? {
53+
val bitmap = when (val bitmap = imageRepository.getBitmap(key = name)) {
54+
null -> createBitmap(name, type, uiMode)
55+
else -> bitmap
56+
}
57+
58+
if (bitmap == null) {
59+
val newBitmap = updateAssetsAndRetry(name, type, uiMode)
60+
return newBitmap?.also {
61+
imageRepository.putBitmap(name, it)
62+
}
63+
} else {
64+
//Save converted bitmap
65+
imageRepository.putBitmap(name, bitmap)
66+
}
67+
68+
return bitmap
69+
}
70+
71+
private fun createSVGBitmap(data: InputStream): Bitmap? {
72+
val svg = SVG.getFromInputStream(data)
73+
return try {
74+
75+
val width = svg.getDocumentWidth() * displayMetrics.density
76+
val height = svg.getDocumentHeight() * displayMetrics.density
77+
78+
// Set the SVG's view box to the desired size
79+
svg.setDocumentWidth(width)
80+
svg.setDocumentHeight(height)
81+
82+
// Create bitmap and canvas
83+
val bitmap = androidx.core.graphics.createBitmap(width.roundToInt(), height.roundToInt())
84+
val canvas = Canvas(bitmap)
85+
86+
// Render SVG to canvas
87+
svg.renderToCanvas(canvas)
88+
89+
bitmap
90+
} catch (e: Exception) {
91+
Logger.e("Error converting SVG to bitmap", e)
92+
null
93+
}
94+
}
95+
96+
private suspend fun createBitmap(name: String, type: Type, uiMode: UiMode): Bitmap? {
97+
val cachedAsset =
98+
assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) ?: return null
99+
return when (type) {
100+
Type.SVG -> createSVGBitmap(cachedAsset.data)
101+
Type.JPG,
102+
Type.WEBP -> BitmapFactory.decodeStream(cachedAsset.data)
103+
}
104+
}
105+
106+
private suspend fun updateAssetsAndRetry(name: String, type: Type, uiMode: UiMode): Bitmap? {
107+
assetRepository.updateAllAssets()
108+
return createBitmap(name, type, uiMode)
109+
}
110+
}
111+
112+
fun assetServiceFactory(
113+
project: Project,
114+
context: Context
115+
): AssetService {
116+
val localDiskDataSource = LocalDiskDataSourceImpl(storageDirectory = project.internalStorageDirectory)
117+
val localMemoryDataSource = LocalMemorySourceImpl()
118+
val imageRepository = ImageRepositoryImpl(
119+
localMemoryDataSource = localMemoryDataSource,
120+
localDiskDataSource = localDiskDataSource
121+
)
122+
123+
val localAssetDataSource = LocalAssetDataSourceImpl(project)
124+
val remoteAssetsSource = RemoteAssetsSourceImpl(project)
125+
val assetRepository = AssetsRepositoryImpl(
126+
remoteAssetsSource = remoteAssetsSource,
127+
localAssetDataSource = localAssetDataSource
128+
)
129+
130+
return AssetServiceImpl(
131+
assetRepository = assetRepository,
132+
imageRepository = imageRepository,
133+
displayMetrics = context.resources.displayMetrics
134+
)
135+
}
136+
137+
fun Context.getUiMode() = if (isDarkMode()) UiMode.NIGHT else UiMode.DAY
138+
139+
// Method 2: Extension function for cleaner usage
140+
private fun Context.isDarkMode(): Boolean {
141+
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
142+
Configuration.UI_MODE_NIGHT_YES -> true
143+
Configuration.UI_MODE_NIGHT_NO -> false
144+
else -> false
145+
}
146+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.snabble.sdk.assetservice.assets.data
2+
3+
import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSource
4+
import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSource
5+
import io.snabble.sdk.assetservice.assets.data.source.dto.AssetDto
6+
import io.snabble.sdk.assetservice.assets.data.source.dto.ManifestDto
7+
import io.snabble.sdk.assetservice.assets.domain.AssetsRepository
8+
import io.snabble.sdk.assetservice.assets.domain.model.Asset
9+
import io.snabble.sdk.assetservice.image.domain.model.Type
10+
import io.snabble.sdk.assetservice.image.domain.model.UiMode
11+
import io.snabble.sdk.utils.Logger
12+
import org.apache.commons.io.FilenameUtils
13+
14+
internal class AssetsRepositoryImpl(
15+
private val remoteAssetsSource: RemoteAssetsSource,
16+
private val localAssetDataSource: LocalAssetDataSource
17+
) : AssetsRepository {
18+
19+
override suspend fun updateAllAssets() {
20+
Logger.d("Start updating all assets. Loading manifest...")
21+
val manifest: ManifestDto = loadManifest() ?: return
22+
23+
removeDeletedAssets(manifest)
24+
25+
Logger.d("Clean up orphaned files...")
26+
localAssetDataSource.cleanupOrphanedFiles()
27+
28+
val newAssets = manifest.files.filterNot { localAssetDataSource.assetExists(it.name) }
29+
Logger.d("Filtered new assets $newAssets")
30+
31+
Logger.d("Continue with loading all new assets...")
32+
val assets: List<AssetDto> = remoteAssetsSource.downloadAllAssets(newAssets)
33+
34+
Logger.d("Saving new assets $assets locally...")
35+
localAssetDataSource.saveMultipleAssets(assets = assets)
36+
}
37+
38+
private suspend fun removeDeletedAssets(manifest: ManifestDto) {
39+
val remoteAssetNames: Set<String> = manifest.files.map { it.name }.toSet()
40+
val deadAssets: List<String> = localAssetDataSource.listAssets().filterNot { it in remoteAssetNames }
41+
Logger.d("Removing deleted assets $deadAssets...")
42+
localAssetDataSource.deleteAsset(deadAssets)
43+
}
44+
45+
private suspend fun loadManifest(): ManifestDto? = remoteAssetsSource.downloadManifest().also {
46+
if (it == null) Logger.e("Manifest couldn't be loaded")
47+
}
48+
49+
override suspend fun loadAsset(name: String, type: Type, uiMode: UiMode): Asset? =
50+
getLocalAsset(filename = name.createFileName(type, uiMode))?.toModel()
51+
52+
private suspend fun getLocalAsset(filename: String): AssetDto? = localAssetDataSource.loadAsset(filename)
53+
54+
private fun String.createFileName(type: Type, uiMode: UiMode): String {
55+
val cleanedName = FilenameUtils.removeExtension(this)
56+
return "$cleanedName${uiMode.value}${type.value}"
57+
}
58+
}
59+
60+
private fun AssetDto.toModel() = Asset(name = name, hash = hash, data = data)

0 commit comments

Comments
 (0)