Skip to content

Commit 7da3004

Browse files
authored
fix: remove Android Gson usage to avoid R8 incompatibility (#126)
## Changes Android widget rendering still used Gson in a few renderer parsing paths, which made the Android release build vulnerable to R8 incompatibility. - replace remaining Android renderer-side Gson parsing with shared `kotlinx.serialization` / `JsonElement` helpers - remove dead Gson leftovers and delete the Android Gson dependency - add Android tests covering renderer JSON decoding and chart mark parsing regressions ## Outcome Android no longer depends on Gson, release builds complete with R8 enabled, and the existing JS wire format remains unchanged. ## Test plan - [x] `npm run format:kotlin:fix` - [x] `npm run test:kotlin` - [x] `./gradlew app:assembleRelease`
1 parent e65b72b commit 7da3004

19 files changed

Lines changed: 778 additions & 152 deletions

packages/voltra/android/build.gradle

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ dependencies {
102102
// Google Tink (encryption for credential storage)
103103
implementation "com.google.crypto.tink:tink-android:1.19.0"
104104

105-
// JSON parsing
106-
implementation "com.google.code.gson:gson:2.10.1"
107-
108105
// Coroutines
109106
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
110107

@@ -114,4 +111,5 @@ dependencies {
114111
// Unit tests
115112
testImplementation "junit:junit:4.13.2"
116113
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1"
114+
testImplementation "org.robolectric:robolectric:4.14.1"
117115
}

packages/voltra/android/src/main/java/voltra/glance/GlanceFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import voltra.models.VoltraNode
99
class GlanceFactory(
1010
private val widgetId: String,
1111
private val sharedElements: List<VoltraNode>? = null,
12-
private val sharedStyles: List<Map<String, Any>>? = null,
12+
private val sharedStyles: List<Map<String, Any?>>? = null,
1313
private val widgetSize: DpSize? = null,
1414
) {
1515
@Composable

packages/voltra/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ object RemoteViewsGenerator {
6262
context: Context,
6363
node: VoltraNode,
6464
sharedElements: List<VoltraNode>?,
65-
sharedStyles: List<Map<String, Any>>?,
65+
sharedStyles: List<Map<String, Any?>>?,
6666
size: DpSize,
6767
): RemoteViews {
6868
// Create a new GlanceRemoteViews instance each time to avoid caching issues

packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ data class ResolvedStyle(
1717

1818
@Composable
1919
fun resolveAndApplyStyle(
20-
props: Map<String, Any>?,
21-
sharedStyles: List<Map<String, Any>>?,
20+
props: Map<String, Any?>?,
21+
sharedStyles: List<Map<String, Any?>>?,
2222
): ResolvedStyle {
2323
val resolvedStyle = resolveStyle(props, sharedStyles)
2424
val compositeStyle =
@@ -42,9 +42,9 @@ fun resolveAndApplyStyle(
4242
* or {"s": {...}} for inline styles.
4343
*/
4444
private fun resolveStyle(
45-
props: Map<String, Any>?,
46-
sharedStyles: List<Map<String, Any>>?,
47-
): Map<String, Any>? {
45+
props: Map<String, Any?>?,
46+
sharedStyles: List<Map<String, Any?>>?,
47+
): Map<String, Any?>? {
4848
if (props == null) return null
4949

5050
val styleRef = props["style"]
@@ -58,7 +58,7 @@ private fun resolveStyle(
5858
is Map<*, *> -> {
5959
// It's already an inline style
6060
@Suppress("UNCHECKED_CAST")
61-
styleRef as? Map<String, Any>
61+
styleRef as? Map<String, Any?>
6262
}
6363

6464
else -> {
@@ -82,7 +82,7 @@ private fun resolveStyle(
8282
@Composable
8383
fun applyClickableIfNeeded(
8484
modifier: GlanceModifier,
85-
props: Map<String, Any>?,
85+
props: Map<String, Any?>?,
8686
elementId: String?,
8787
widgetId: String,
8888
componentType: Int,

packages/voltra/android/src/main/java/voltra/glance/VoltraRenderContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import voltra.models.VoltraNode
77
data class VoltraRenderContext(
88
val widgetId: String,
99
val sharedElements: List<VoltraNode>? = null,
10-
val sharedStyles: List<Map<String, Any>>? = null,
10+
val sharedStyles: List<Map<String, Any?>>? = null,
1111
val widgetSize: DpSize? = null,
1212
)
1313

packages/voltra/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@ import androidx.glance.appwidget.components.OutlineButton
1313
import androidx.glance.appwidget.components.SquareIconButton
1414
import androidx.glance.layout.Box
1515
import androidx.glance.unit.ColorProvider
16-
import com.google.gson.Gson
1716
import voltra.glance.LocalVoltraRenderContext
1817
import voltra.glance.applyClickableIfNeeded
1918
import voltra.glance.resolveAndApplyStyle
2019
import voltra.models.VoltraElement
2120
import voltra.styling.JSColorParser
2221
import voltra.styling.toColorProvider
2322

24-
private const val TAG = "ButtonRenderers"
25-
private val gson = Gson()
26-
2723
@Composable
2824
fun RenderButton(
2925
element: VoltraElement,

packages/voltra/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import android.graphics.DashPathEffect
66
import android.graphics.Paint
77
import android.graphics.Path
88
import android.graphics.RectF
9-
import android.util.Log
109
import androidx.compose.ui.graphics.toArgb
1110
import voltra.styling.JSColorParser
1211
import voltra.styling.VoltraColorValue
1312

14-
private const val TAG = "ChartBitmapRenderer"
15-
1613
private val DEFAULT_PALETTE =
1714
intArrayOf(
1815
0xFF4E79A7.toInt(), // blue
@@ -46,24 +43,19 @@ data class SectorPoint(
4643
)
4744

4845
fun parseMarksJson(marksJson: String): List<WireMark> {
49-
return try {
50-
val gson = com.google.gson.Gson()
51-
val type = object : com.google.gson.reflect.TypeToken<List<List<Any>>>() {}.type
52-
val outer: List<List<Any>> = gson.fromJson(marksJson, type)
53-
outer.mapNotNull { row ->
54-
if (row.size < 3) return@mapNotNull null
55-
val markType = row[0] as? String ?: return@mapNotNull null
56-
57-
@Suppress("UNCHECKED_CAST")
58-
val data = row[1] as? List<List<Any>>
59-
60-
@Suppress("UNCHECKED_CAST")
61-
val props = (row[2] as? Map<String, Any>) ?: emptyMap()
62-
WireMark(markType, data, props)
63-
}
64-
} catch (e: Exception) {
65-
Log.w(TAG, "Failed to parse marks JSON", e)
66-
emptyList()
46+
return parseMarksTuples(marksJson).mapNotNull { row ->
47+
if (row.size < 3) return@mapNotNull null
48+
val markType = row[0] as? String ?: return@mapNotNull null
49+
50+
@Suppress("UNCHECKED_CAST")
51+
val data =
52+
(row[1] as? List<*>)?.mapNotNull { point ->
53+
(point as? List<*>)?.toList() as? List<Any>
54+
}
55+
56+
@Suppress("UNCHECKED_CAST")
57+
val props = row[2] as? Map<String, Any> ?: emptyMap()
58+
WireMark(markType, data, props)
6759
}
6860
}
6961

packages/voltra/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,7 @@ fun RenderChart(
152152

153153
@Composable
154154
private fun parseForegroundStyleScale(json: String?): Map<String, Int>? {
155-
if (json.isNullOrEmpty()) return null
156-
val pairs: List<List<String>> =
157-
try {
158-
val gson = com.google.gson.Gson()
159-
val type = object : com.google.gson.reflect.TypeToken<List<List<String>>>() {}.type
160-
gson.fromJson(json, type)
161-
} catch (e: Exception) {
162-
Log.w(TAG, "Failed to parse foregroundStyleScale", e)
163-
return null
164-
}
155+
val pairs = parseForegroundStyleScaleEntries(json) ?: return null
165156

166157
val map = mutableMapOf<String, Int>()
167158
for (pair in pairs) {

packages/voltra/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import androidx.glance.LocalContext
99
import androidx.glance.appwidget.components.Scaffold
1010
import androidx.glance.appwidget.components.TitleBar
1111
import androidx.glance.text.FontFamily
12-
import com.google.gson.Gson
1312
import voltra.glance.LocalVoltraRenderContext
1413
import voltra.glance.applyClickableIfNeeded
1514
import voltra.glance.resolveAndApplyStyle
@@ -19,9 +18,6 @@ import voltra.payload.ComponentTypeID
1918
import voltra.styling.JSColorParser
2019
import voltra.styling.toColorProvider
2120

22-
private const val TAG = "ComplexRenderers"
23-
private val gson = Gson()
24-
2521
@Composable
2622
fun RenderTitleBar(
2723
element: VoltraElement,

packages/voltra/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,15 @@ fun RenderLazyVerticalGrid(
132132
}
133133
}
134134

135-
private fun extractHorizontalAlignment(props: Map<String, Any>?): Alignment.Horizontal =
135+
private fun extractHorizontalAlignment(props: Map<String, Any?>?): Alignment.Horizontal =
136136
when (props?.get("horizontalAlignment") as? String) {
137137
"start" -> Alignment.Horizontal.Start
138138
"center-horizontally" -> Alignment.Horizontal.CenterHorizontally
139139
"end" -> Alignment.Horizontal.End
140140
else -> Alignment.Horizontal.Start
141141
}
142142

143-
private fun extractVerticalAlignment(props: Map<String, Any>?): Alignment.Vertical =
143+
private fun extractVerticalAlignment(props: Map<String, Any?>?): Alignment.Vertical =
144144
when (props?.get("verticalAlignment") as? String) {
145145
"top" -> Alignment.Vertical.Top
146146
"center" -> Alignment.Vertical.CenterVertically

0 commit comments

Comments
 (0)