Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion core/src/main/java/io/snabble/sdk/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -357,6 +359,9 @@ class Project internal constructor(
lateinit var assets: Assets
private set

lateinit var assetService: AssetService
private set

var appTheme: AppTheme? = null
private set

Expand Down Expand Up @@ -567,6 +572,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 }
Expand All @@ -579,7 +586,6 @@ class Project internal constructor(
coupons.setProjectCoupons(couponList)
}
coupons.update()

notifyUpdate()
}

Expand Down
146 changes: 146 additions & 0 deletions core/src/main/java/io/snabble/sdk/assetservice/AssetService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@file:Suppress("TooGenericExceptionCaught")

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.assets.data.AssetsRepositoryImpl
import io.snabble.sdk.assetservice.assets.data.source.LocalAssetDataSourceImpl
import io.snabble.sdk.assetservice.assets.data.source.RemoteAssetsSourceImpl
import io.snabble.sdk.assetservice.assets.domain.AssetsRepository
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
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?
}

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
}

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
}

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? {
val cachedAsset =
assetRepository.loadAsset(name = name, type = type, uiMode = uiMode) ?: 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): Bitmap? {
assetRepository.updateAllAssets()
return createBitmap(name, type, 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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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.assets.domain.model.Asset
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

internal 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<AssetDto> = remoteAssetsSource.downloadAllAssets(newAssets)

Logger.d("Saving new assets $assets locally...")
localAssetDataSource.saveMultipleAssets(assets = assets)
}

private suspend fun removeDeletedAssets(manifest: ManifestDto) {
val remoteAssetNames: Set<String> = manifest.files.map { it.name }.toSet()
val deadAssets: List<String> = 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): Asset? =
getLocalAsset(filename = name.createFileName(type, uiMode))?.toModel()

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}"
}
}

private fun AssetDto.toModel() = Asset(name = name, hash = hash, data = data)
Loading