Skip to content

Commit

Permalink
feat: Added cart sharing with LZ4 compression and MessagePack seriali…
Browse files Browse the repository at this point in the history
…zation, improved empty state, and updated dependencies

- Implemented cart sharing functionality using LZ4 compression and MessagePack serialization.
- Refactored `HomeRepositoryImplementation` to handle the new cart sharing format:
    - Added `decodeBase62` to decode Base62-encoded data.
    - Added `decompressLZ4` to decompress LZ4-compressed data.
    - Added `deserializeMessagePack` to deserialize MessagePack-serialized data.
    - Updated `importSharedCartImplementation` to use the new decoding and deserialization methods.
- Refactored `CartRepositoryImplementation` to use the new cart sharing format:
    - Added `serializeToMessagePack` to serialize cart and item data to MessagePack.
    - Added `compressLZ4` to compress data using LZ4.
    - Added `encodeBase62` to encode compressed data to Base62.
    - Updated `generateCartShareLinkImplementation` to use the new serialization, compression, and encoding methods.
- Added a new `NoCartsScreen` composable to display a message and ad when no carts are available.
- Updated the home screen to use the new `NoCartsScreen` when no carts are available.
- Updated `CartItem` to format prices using the default locale.
- Updated dependencies versions in `libs.versions.toml` and `build.gradle.kts`.
- Added `msgpack-core` and `lz4-java` dependencies.
- Updated the application's version code to 86.
  • Loading branch information
Mihai-Cristian Condrea committed Feb 25, 2025
1 parent 336847d commit e24ef68
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 53 deletions.
9 changes: 6 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ android {
applicationId = "com.d4rk.cartcalculator"
minSdk = 23
targetSdk = 35
versionCode = 85
versionCode = 86
versionName = "1.2.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@Suppress("UnstableApiUsage")
Expand Down Expand Up @@ -79,7 +79,6 @@ android {
compose = true
}


packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
Expand All @@ -96,10 +95,14 @@ android {
dependencies {

// App Core
implementation(dependencyNotation = "com.github.D4rK7355608:AppToolkit:0.0.67") {
implementation(dependencyNotation = "com.github.D4rK7355608:AppToolkit:0.0.68") {
isTransitive = true
}

// Compression
implementation(dependencyNotation = libs.msgpack.core)
implementation(dependencyNotation = libs.lz4.java)

// KSP
ksp(dependencyNotation = libs.androidx.room.compiler)
implementation(dependencyNotation = libs.androidx.room.ktx)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.d4rk.cartcalculator.ui.components.layouts

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.d4rk.android.libs.apptoolkit.ui.components.spacers.LargeVerticalSpacer
import com.d4rk.cartcalculator.R
import com.d4rk.cartcalculator.ui.components.ads.AdBanner
import com.google.android.gms.ads.AdSize

@Composable
fun NoCartsScreen() {
Text(
text = stringResource(id = R.string.no_carts_available)
)
LargeVerticalSpacer()
AdBanner(
modifier = Modifier
.fillMaxWidth()
.height(AdSize.MEDIUM_RECTANGLE.height.dp) , adSize = AdSize.MEDIUM_RECTANGLE
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ fun CartScreen(activity : CartActivity , cartId : Int) {
Row {
Text(
text = String.format(
Locale.US , "%.1f" , uiState.totalPrice.toFloat()
Locale.getDefault() , "%.1f" , uiState.totalPrice.toFloat()
).removeSuffix(".0") , style = MaterialTheme.typography.headlineSmall , fontWeight = FontWeight.Bold
)
SmallHorizontalSpacer()
Expand Down Expand Up @@ -394,7 +394,7 @@ fun CartItemComposable(
)
Row {
Text(
text = String.format(Locale.US , "%.1f" , cartItem.price.toFloat()).removeSuffix(suffix = ".0") ,
text = String.format(Locale.getDefault() , "%.1f" , cartItem.price.toFloat()).removeSuffix(suffix = ".0") ,
)
Spacer(modifier = Modifier.width(4.dp))
Text(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.d4rk.cartcalculator.ui.screens.cart.repository

import android.util.Base64
import com.d4rk.cartcalculator.data.core.AppCoreManager
import com.d4rk.cartcalculator.data.database.table.ShoppingCartItemsTable
import com.d4rk.cartcalculator.data.database.table.ShoppingCartTable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.jpountz.lz4.LZ4Compressor
import net.jpountz.lz4.LZ4Factory
import org.msgpack.core.MessagePack
import org.msgpack.core.MessagePacker
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPOutputStream
import java.math.BigInteger
import java.net.URLEncoder
import java.util.Locale

abstract class CartRepositoryImplementation {

Expand All @@ -29,30 +32,101 @@ abstract class CartRepositoryImplementation {
AppCoreManager.database.shoppingCartItemsDao().update(item = cartItems)
}

suspend fun generateCartShareLinkImplementation(cartId : Int) : String? {
suspend fun generateCartShareLinkImplementation(cartIdentifier : Int) : String? {
return runCatching {
val cart : ShoppingCartTable? = loadCartIdImplementation(cartId)
val items : List<ShoppingCartItemsTable> = fetchItemsForCartImplementation(cartId)
cart?.let {
val cartData : Map<String , String> = mapOf(
"cart" to Json.encodeToString(it) , "items" to Json.encodeToString(items)
)
val encodedData : String = encodeBase64UrlSafe(compressJson(Json.encodeToString(cartData)))
println("Encoded Data: $encodedData")
"https://cartcalculator.com/import?data=$encodedData"
val shoppingCart : ShoppingCartTable? = loadCartIdImplementation(cartIdentifier)
val cartItems : List<ShoppingCartItemsTable> = fetchItemsForCartImplementation(cartIdentifier)

if (shoppingCart == null) {
println("🚨 ShoppingCart is null for cartId: $cartIdentifier")
return@runCatching null
}
if (cartItems.isEmpty()) {
println("⚠️ No items found for cartId: $cartIdentifier")
}

println("🛒 Cart: $shoppingCart")
println("📦 Items: $cartItems")

shoppingCart.let {
val serializedCart : ByteArray = serializeToMessagePack(shoppingCart , cartItems)
println("🔍 Serialized Cart Size: ${serializedCart.size} bytes")

val compressedCartData : ByteArray = compressLZ4(serializedCart)
println("📦 Compressed Cart Size: ${compressedCartData.size} bytes")

if (compressedCartData.isEmpty()) {
println("🚨 Compressed data is empty!")
return@runCatching null
}

val encodedData : String = encodeBase62(compressedCartData)
if (encodedData.isEmpty()) {
println("🚨 Encoded data is empty!")
return@runCatching null
}

val urlEncodedCartData : String = URLEncoder.encode(encodedData , "UTF-8")
println("✅ Final Encoded Data: $urlEncodedCartData")
"https://cartcalculator.com/import?d=$urlEncodedCartData"
}
}.getOrElse {
println("❌ Error in generating share link: ${it.message}")
null
}
}

private fun encodeBase64UrlSafe(input : ByteArray) : String {
return Base64.encodeToString(input , Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
private fun serializeToMessagePack(shoppingCart : ShoppingCartTable , cartItems : List<ShoppingCartItemsTable>) : ByteArray {
val outputStream = ByteArrayOutputStream()
val messagePacker : MessagePacker = MessagePack.newDefaultPacker(outputStream)

messagePacker.packArrayHeader(3)
messagePacker.packInt(shoppingCart.cartId)
messagePacker.packString(shoppingCart.name)
messagePacker.packLong(shoppingCart.date)

messagePacker.packArrayHeader(cartItems.size)
for (cartItem in cartItems) {
messagePacker.packArrayHeader(4)
messagePacker.packInt(cartItem.itemId)
messagePacker.packString(cartItem.name)
messagePacker.packInt(cartItem.quantity)
messagePacker.packFloat(cartItem.price.toFloat())
}

messagePacker.close()
val data = outputStream.toByteArray()
println("✅ MessagePack Data Size: ${data.size} bytes")
return data
}

private fun compressJson(json : String) : ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
GZIPOutputStream(byteArrayOutputStream).use { it.write(json.toByteArray()) }
return byteArrayOutputStream.toByteArray()
private fun compressLZ4(uncompressedData : ByteArray) : ByteArray {
val factory : LZ4Factory = LZ4Factory.fastestInstance()
val compressor : LZ4Compressor = factory.fastCompressor()
val maxCompressedLength : Int = compressor.maxCompressedLength(uncompressedData.size)
val compressedData = ByteArray(maxCompressedLength)
val actualCompressedSize : Int = compressor.compress(uncompressedData , 0 , uncompressedData.size , compressedData , 0 , maxCompressedLength)

val finalData = ByteArray(size = 4 + actualCompressedSize)
finalData[0] = (uncompressedData.size shr 24).toByte()
finalData[1] = (uncompressedData.size shr 16).toByte()
finalData[2] = (uncompressedData.size shr 8).toByte()
finalData[3] = (uncompressedData.size).toByte()
System.arraycopy(compressedData , 0 , finalData , 4 , actualCompressedSize)
return finalData
}

private fun encodeBase62(input : ByteArray) : String {
if (input.isEmpty()) return ""
val characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var numericValue = BigInteger(1 , input)
val encoded : StringBuilder = StringBuilder()
while (numericValue > BigInteger.ZERO) {
val index : BigInteger = numericValue.mod(BigInteger.valueOf(62))
encoded.insert(0 , characters[index.toInt()])
numericValue = numericValue.divide(BigInteger.valueOf(62))
}
val dataSizePrefix : String = String.format(Locale.getDefault(), "%04d" , input.size)
return dataSizePrefix + encoded.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.d4rk.cartcalculator.ui.components.ads.AdBanner
import com.d4rk.cartcalculator.ui.components.dialogs.AddNewCartAlertDialog
import com.d4rk.cartcalculator.ui.components.dialogs.DeleteCartAlertDialog
import com.d4rk.cartcalculator.ui.components.dialogs.ImportCartAlertDialog
import com.d4rk.cartcalculator.ui.components.layouts.NoCartsScreen
import com.d4rk.cartcalculator.ui.components.modifiers.hapticSwipeToDismissBox
import com.google.android.gms.ads.AdSize
import java.text.SimpleDateFormat
Expand Down Expand Up @@ -102,9 +103,7 @@ fun HomeScreen(
CircularProgressIndicator()
}
else if (uiState.carts.isEmpty()) {
Text(
text = stringResource(id = R.string.no_carts_available)
)
NoCartsScreen()
}
else {
val carts : List<ShoppingCartTable> = uiState.carts
Expand Down
Loading

0 comments on commit e24ef68

Please sign in to comment.