Skip to content

Commit 85571cf

Browse files
authored
fix(accesskey): make export dimensions compile-time constants (#1039)
The access key export dimensions (targetWidth/Height, logo, QR, text offsets) were instance `val`s, read by createBitmapForExport on dispatchers.IO. That work is kicked off from the init{} flow collector before the constructor finishes initializing the fields, publishing a half-built `this` to a background thread with no happens-before. The IO thread could then observe a field as its default 0, throwing "IllegalArgumentException: width and height must be > 0" — but only on release/R8 builds (isDebuggable=false); debug and minified-debug masked it via un-optimized codegen and timing. Promote the dimensions to `const val` in a companion object so kotlinc inlines them at every use site — no instance fields left to race on. Also switch the entropy flow to mapNotNull. Refs Bugsnag 6a457e5e. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 6641fd0 commit 85571cf

1 file changed

Lines changed: 42 additions & 35 deletions

File tree

apps/flipcash/shared/accesskey/src/main/kotlin/com/flipcash/app/accesskey/BaseAccessKeyViewModel.kt

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@ import androidx.lifecycle.ViewModel
3232
import com.getcode.view.LoadingSuccessState
3333
import kotlinx.coroutines.flow.MutableStateFlow
3434
import kotlinx.coroutines.flow.distinctUntilChangedBy
35-
import kotlinx.coroutines.flow.filterNotNull
3635
import kotlinx.coroutines.flow.launchIn
37-
import kotlinx.coroutines.flow.map
3836
import kotlinx.coroutines.flow.onEach
3937
import kotlinx.coroutines.flow.take
4038
import kotlinx.coroutines.flow.update
4139
import kotlinx.coroutines.launch
4240
import kotlinx.coroutines.withContext
4341
import com.getcode.vendor.Base58
42+
import kotlinx.coroutines.flow.mapNotNull
4443
import java.text.DateFormat
4544
import java.text.SimpleDateFormat
4645
import java.util.Date
@@ -72,8 +71,7 @@ abstract class BaseAccessKeyViewModel(
7271
init {
7372
userManager.state
7473
.distinctUntilChangedBy { it.entropy }
75-
.map { it.entropy }
76-
.filterNotNull()
74+
.mapNotNull { it.entropy }
7775
.take(1)
7876
.onEach { initWithEntropy(it) }
7977
.launchIn(viewModelScope)
@@ -127,19 +125,28 @@ abstract class BaseAccessKeyViewModel(
127125
)
128126
}
129127

130-
private val targetWidth = 1200
131-
private val targetHeight = 2500
132-
133-
private val logoWidth = 92.4f
134-
private val logoHeight = 132
135-
private val qrCodeSize = 360
136-
137-
private val bgTopOffset = 550
138-
private val logoTopOffset = 770
139-
private val qrTopOffset = 980
140-
private val keyTextTopOffset = 1600
141-
private val topTextTopOffset = 200
142-
private val bottomTextTopOffset = 2000
128+
// These layout dimensions are compile-time constants so they are inlined at every
129+
// use site by kotlinc. They must NOT be instance fields: createBitmapForExport runs
130+
// on dispatchers.IO and can be kicked off from the init{} flow before the constructor
131+
// finishes initializing instance fields, so a background thread could read them as 0
132+
// (their default) with no happens-before — surfacing as
133+
// "IllegalArgumentException: width and height must be > 0" on release/R8 builds only.
134+
// Refs Bugsnag 6a457e5e.
135+
private companion object {
136+
const val TARGET_WIDTH = 1200
137+
const val TARGET_HEIGHT = 2500
138+
139+
const val LOGO_WIDTH_RATIO = 92.4f
140+
const val LOGO_HEIGHT = 132
141+
const val QR_CODE_SIZE = 360
142+
143+
const val BG_TOP_OFFSET = 550
144+
const val LOGO_TOP_OFFSET = 770
145+
const val QR_TOP_OFFSET = 980
146+
const val KEY_TEXT_TOP_OFFSET = 1600
147+
const val WARNING_TEXT_TOP_OFFSET = 200
148+
const val HELPER_TEXT_TOP_OFFSET = 2000
149+
}
143150

144151
protected suspend fun saveBitmapToFile(): Result<Boolean> {
145152
uiFlow.update { it.copy(exportState = LoadingSuccessState(loading = true)) }
@@ -159,7 +166,7 @@ abstract class BaseAccessKeyViewModel(
159166
}
160167

161168
private fun renderLogo(resId: Int): Bitmap? =
162-
resources.getDrawable(resId)?.toBitmap(logoWidth.roundToInt(), logoHeight)
169+
resources.getDrawable(resId)?.toBitmap(LOGO_WIDTH_RATIO.roundToInt(), LOGO_HEIGHT)
163170

164171
private fun createBitmapForExport(
165172
drawBackground: Boolean = true,
@@ -190,7 +197,7 @@ abstract class BaseAccessKeyViewModel(
190197
}
191198
.getOrNull()
192199

193-
val imageOut = createBitmap(targetWidth, targetHeight).applyCanvas {
200+
val imageOut = createBitmap(TARGET_WIDTH, TARGET_HEIGHT).applyCanvas {
194201
val accessBgActualWidth =
195202
accessKeyBg.getScaledWidth(resources.displayMetrics)
196203

@@ -209,7 +216,7 @@ abstract class BaseAccessKeyViewModel(
209216
topTextChunks.forEachIndexed { index, text ->
210217
drawText(
211218
canvas = this,
212-
y = topTextTopOffset + (60 * (index + 1)),
219+
y = WARNING_TEXT_TOP_OFFSET + (60 * (index + 1)),
213220
sizePx = 40,
214221
color = Alert.toAGColor(),
215222
text = text
@@ -218,8 +225,8 @@ abstract class BaseAccessKeyViewModel(
218225

219226
drawBitmap(
220227
accessKeyBg,
221-
(((targetWidth - accessBgActualWidth) / 2)).toFloat(),
222-
bgTopOffset.toFloat(),
228+
(((TARGET_WIDTH - accessBgActualWidth) / 2)).toFloat(),
229+
BG_TOP_OFFSET.toFloat(),
223230
null
224231
)
225232

@@ -234,10 +241,10 @@ abstract class BaseAccessKeyViewModel(
234241

235242
val borderInset = spec.borderWidth / 2
236243
val borderRect = RectF(
237-
((targetWidth - accessBgActualWidth) / 2f) + borderInset,
238-
bgTopOffset.toFloat() + borderInset,
239-
((targetWidth + accessBgActualWidth) / 2f) - borderInset,
240-
bgTopOffset.toFloat() + accessKeyBg.height - borderInset
244+
((TARGET_WIDTH - accessBgActualWidth) / 2f) + borderInset,
245+
BG_TOP_OFFSET.toFloat() + borderInset,
246+
((TARGET_WIDTH + accessBgActualWidth) / 2f) - borderInset,
247+
BG_TOP_OFFSET.toFloat() + accessKeyBg.height - borderInset
241248
)
242249

243250
// Adjust corner radius to match your card
@@ -248,32 +255,32 @@ abstract class BaseAccessKeyViewModel(
248255
imageLogo?.let { logo ->
249256
drawBitmap(
250257
logo,
251-
((targetWidth - logoWidth) / 2),
252-
logoTopOffset.toFloat(),
258+
((TARGET_WIDTH - LOGO_WIDTH_RATIO) / 2),
259+
LOGO_TOP_OFFSET.toFloat(),
253260
null
254261
)
255262
}
256263

257264
getQrCode(entropyB64)?.let { bitmap ->
258265
drawBitmap(
259266
bitmap,
260-
((targetWidth - qrCodeSize) / 2).toFloat(),
261-
qrTopOffset.toFloat(),
267+
((TARGET_WIDTH - QR_CODE_SIZE) / 2).toFloat(),
268+
QR_TOP_OFFSET.toFloat(),
262269
null
263270
)
264271
}
265272

266273
drawText(
267274
canvas = this,
268-
y = keyTextTopOffset,
275+
y = KEY_TEXT_TOP_OFFSET,
269276
sizePx = 32,
270277
color = White.toAGColor(),
271278
text = accessKeyText[0]
272279
)
273280

274281
drawText(
275282
canvas = this,
276-
y = keyTextTopOffset + 40,
283+
y = KEY_TEXT_TOP_OFFSET + 40,
277284
sizePx = 32,
278285
color = White.toAGColor(),
279286
text = accessKeyText[1]
@@ -287,7 +294,7 @@ abstract class BaseAccessKeyViewModel(
287294
bottomTextChunks.forEachIndexed { index, text ->
288295
drawText(
289296
canvas = this,
290-
y = bottomTextTopOffset + (60 * (index + 1)),
297+
y = HELPER_TEXT_TOP_OFFSET + (60 * (index + 1)),
291298
sizePx = 40,
292299
color = White.toAGColor(),
293300
text = text
@@ -335,7 +342,7 @@ abstract class BaseAccessKeyViewModel(
335342
val base58 = Base58.encode(entropyB64.decodeBase64())
336343
val url = "${resources.getString(R.string.app_root_url)}/login?data=$base58"
337344

338-
return qrCodeGenerator.generate(url, qrCodeSize)
345+
return qrCodeGenerator.generate(url, QR_CODE_SIZE)
339346
}
340347

341348
private fun drawText(
@@ -356,7 +363,7 @@ abstract class BaseAccessKeyViewModel(
356363

357364
val bounds1 = android.graphics.Rect()
358365
textPaint.getTextBounds(text, 0, text.length, bounds1)
359-
val xV: Int = x ?: ((targetWidth - bounds1.width()) / 2)
366+
val xV: Int = x ?: ((TARGET_WIDTH - bounds1.width()) / 2)
360367
canvas.drawText(text, xV.toFloat(), y.toFloat(), textPaint)
361368
}
362369

0 commit comments

Comments
 (0)