Use bundles for decoded scan handoff

Replace Scan parcelables in the decode navigation flow with plain
Bundle data.

This avoids BadParcelableException when Android lazily unparcels a
Scan written with a different layout or legacy ZXingCpp format names.
A bundle of primitives is stable across app updates and does not depend
on custom parcel compatibility.
This commit is contained in:
Markus Fisch
2026-03-12 21:44:57 +01:00
parent 76e82ed842
commit a38b7b647f
3 changed files with 235 additions and 32 deletions
@@ -16,6 +16,8 @@ import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.database.toBundle
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
@@ -81,6 +83,7 @@ class MainActivity : AppCompatActivity() {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
const val DECODED_SCAN = "decoded_scan"
const val DECODED = "decoded"
private fun Intent.getFragmentForIntent(): Fragment? = when {
@@ -101,7 +104,11 @@ class MainActivity : AppCompatActivity() {
getStringExtra(ENCODE)
)
hasExtra(DECODED) -> getScanExtra(DECODED)?.let {
hasExtra(DECODED_SCAN) -> getScanBundleExtra(DECODED_SCAN)?.let {
DecodeFragment.newInstance(it)
}
hasExtra(DECODED) -> getLegacyScanExtra(DECODED)?.let {
DecodeFragment.newInstance(it)
}
@@ -139,14 +146,18 @@ class MainActivity : AppCompatActivity() {
fun getDecodeIntent(context: Context, scan: Scan): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODED, scan)
intent.putExtra(DECODED_SCAN, scan.toBundle())
return intent
}
}
}
@Suppress("DEPRECATION")
private fun Intent.getScanExtra(name: String): Scan? = if (
private fun Intent.getScanBundleExtra(name: String) = getBundleExtra(name)
?.toScan()
@Suppress("DEPRECATION")
private fun Intent.getLegacyScanExtra(name: String): Scan? = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) {
try {
@@ -163,4 +174,8 @@ private fun Intent.getScanExtra(name: String): Scan? = if (
}
} else {
null
} ?: getParcelableExtra(name)
} ?: try {
getParcelableExtra(name)
} catch (_: Throwable) {
null
}
@@ -1,5 +1,6 @@
package de.markusfisch.android.binaryeye.database
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.text.format.DateFormat
@@ -7,6 +8,7 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import java.util.Locale
data class Scan(
@@ -80,26 +82,6 @@ data class Scan(
return result
}
private constructor(parcel: Parcel) : this(
text = parcel.readString() ?: "",
raw = parcel.readSizedByteArray(),
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
errorCorrectionLevel = parcel.readString(),
version = parcel.readString(),
dataMask = parcel.readInt(),
symbol = parcel.readBitMatrix(),
sequenceSize = parcel.readInt(),
sequenceIndex = parcel.readInt(),
sequenceId = parcel.readString() ?: "",
country = parcel.readString(),
addOn = parcel.readString(),
price = parcel.readString(),
issueNumber = parcel.readString(),
dateTime = parcel.readString() ?: "",
id = parcel.readLong(),
label = parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.apply {
writeString(text)
@@ -127,7 +109,9 @@ data class Scan(
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<Scan> {
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
override fun createFromParcel(parcel: Parcel) = readScanFromParcel(
parcel
)
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
}
}
@@ -158,6 +142,58 @@ fun Result.toScan(): Scan {
)
}
fun Scan.toBundle() = Bundle().apply {
putString(SCAN_TEXT, text)
putByteArray(SCAN_RAW, raw)
putString(SCAN_FORMAT, format.name)
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
putString(SCAN_VERSION, version)
putInt(SCAN_DATA_MASK, dataMask)
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
putString(SCAN_SEQUENCE_ID, sequenceId)
putString(SCAN_COUNTRY, country)
putString(SCAN_ADD_ON, addOn)
putString(SCAN_PRICE, price)
putString(SCAN_ISSUE_NUMBER, issueNumber)
putString(SCAN_DATE_TIME, dateTime)
putLong(SCAN_ID, id)
putString(SCAN_LABEL, label)
}
fun Bundle.toScan(): Scan? {
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
if (formatName.isNullOrEmpty()) {
return null
}
return try {
Scan(
text = getString(SCAN_TEXT) ?: "",
raw = getByteArray(SCAN_RAW),
format = BarcodeFormat.valueOf(formatName),
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
version = getString(SCAN_VERSION),
dataMask = getInt(SCAN_DATA_MASK, -1),
symbol = getBitMatrix(),
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
country = getString(SCAN_COUNTRY),
addOn = getString(SCAN_ADD_ON),
price = getString(SCAN_PRICE),
issueNumber = getString(SCAN_ISSUE_NUMBER),
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
id = getLong(SCAN_ID, 0L),
label = getString(SCAN_LABEL)
)
} catch (_: IllegalArgumentException) {
null
}
}
private fun getDateTime(
time: Long = System.currentTimeMillis()
) = DateFormat.format(
@@ -169,6 +205,139 @@ private fun getDateTime(
time % 1000
)
private fun readScanFromParcel(parcel: Parcel): Scan {
val text = parcel.readString() ?: ""
val raw = parcel.readSizedByteArray()
val format = parcel.readBarcodeFormat()
val errorCorrectionLevel = parcel.readString()
val version = parcel.readString()
val dataMask = parcel.readInt()
val tail = parcel.readScanParcelTail()
return Scan(
text = text,
raw = raw,
format = format,
errorCorrectionLevel = errorCorrectionLevel,
version = version,
dataMask = dataMask,
symbol = tail.symbol,
sequenceSize = tail.sequenceSize,
sequenceIndex = tail.sequenceIndex,
sequenceId = tail.sequenceId,
country = tail.country,
addOn = tail.addOn,
price = tail.price,
issueNumber = tail.issueNumber,
dateTime = tail.dateTime,
id = tail.id,
label = tail.label
)
}
private data class ScanParcelTail(
val symbol: BitMatrix?,
val sequenceSize: Int,
val sequenceIndex: Int,
val sequenceId: String,
val country: String?,
val addOn: String?,
val price: String?,
val issueNumber: String?,
val dateTime: String,
val id: Long,
val label: String?
)
private fun Parcel.readBarcodeFormat(): BarcodeFormat {
val formatName = (readString() ?: "").migrateBarcodeFormatName()
return BarcodeFormat.valueOf(formatName)
}
private fun Parcel.readScanParcelTail(): ScanParcelTail {
val position = dataPosition()
val tail = readCurrentScanParcelTail()
if (tail.isPlausible()) {
return tail
}
setDataPosition(position)
return readLegacyScanParcelTail()
}
private fun Parcel.readCurrentScanParcelTail() = ScanParcelTail(
symbol = readBitMatrix(),
sequenceSize = readInt(),
sequenceIndex = readInt(),
sequenceId = readString() ?: "",
country = readString(),
addOn = readString(),
price = readString(),
issueNumber = readString(),
dateTime = readString() ?: "",
id = readLong(),
label = readString()
)
private fun Parcel.readLegacyScanParcelTail() = ScanParcelTail(
symbol = null,
sequenceSize = readInt(),
sequenceIndex = readInt(),
sequenceId = readString() ?: "",
country = readString(),
addOn = readString(),
price = readString(),
issueNumber = readString(),
dateTime = readString() ?: "",
id = readLong(),
label = readString()
)
private fun ScanParcelTail.isPlausible(): Boolean {
if (!dateTime.matches(DATE_TIME_PATTERN)) {
return false
}
if (id < 0L) {
return false
}
if (sequenceSize < -1 || sequenceIndex < -1) {
return false
}
return true
}
private val DATE_TIME_PATTERN = Regex(
"""^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\d{3}$"""
)
private fun Bundle.getBitMatrix(): BitMatrix? {
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
val data = getByteArray(SCAN_SYMBOL_DATA)
if (width < 1 || height < 1 || data == null) {
return null
}
return BitMatrix(width, height, data)
}
private const val SCAN_TEXT = "text"
private const val SCAN_RAW = "raw"
private const val SCAN_FORMAT = "format"
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
private const val SCAN_VERSION = "version"
private const val SCAN_DATA_MASK = "data_mask"
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
private const val SCAN_SYMBOL_DATA = "symbol_data"
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
private const val SCAN_SEQUENCE_ID = "sequence_id"
private const val SCAN_COUNTRY = "country"
private const val SCAN_ADD_ON = "add_on"
private const val SCAN_PRICE = "price"
private const val SCAN_ISSUE_NUMBER = "issue_number"
private const val SCAN_DATE_TIME = "date_time"
private const val SCAN_ID = "id"
private const val SCAN_LABEL = "label"
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
val size = array?.size ?: 0
writeInt(size)
@@ -54,6 +54,8 @@ import de.markusfisch.android.binaryeye.content.toBarcode
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.database.toBundle
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
@@ -135,15 +137,14 @@ class DecodeFragment : Fragment() {
val justScanned = activity?.intent?.hasExtra(
MainActivity.DECODED
) == true || activity?.intent?.hasExtra(
MainActivity.DECODED_SCAN
) == true
closeAutomatically = prefs.closeAutomatically && justScanned
scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable(SCAN, Scan::class.java)
} else {
@Suppress("DEPRECATION")
arguments?.getParcelable(SCAN)
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
scan = arguments?.getBundle(SCAN_BUNDLE)?.toScan()
?: arguments?.getLegacyScan(SCAN)
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
isBinary = scan.text.isEmpty()
originalBytes = scan.raw ?: scan.text.toByteArray()
@@ -667,12 +668,13 @@ class DecodeFragment : Fragment() {
companion object {
private const val SCAN = "scan"
private const val SCAN_BUNDLE = "scan_bundle"
private const val OPEN_DOCUMENT = 1
private const val SCHEME_FILE = "file://"
fun newInstance(scan: Scan): Fragment {
val args = Bundle()
args.putParcelable(SCAN, scan)
args.putBundle(SCAN_BUNDLE, scan.toBundle())
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
@@ -680,6 +682,23 @@ class DecodeFragment : Fragment() {
}
}
@Suppress("DEPRECATION")
private fun Bundle.getLegacyScan(name: String): Scan? = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) {
try {
getParcelable(name, Scan::class.java)
} catch (_: Throwable) {
null
}
} else {
null
} ?: try {
getParcelable(name)
} catch (_: Throwable) {
null
}
private inline fun <T : View> T.showIf(
visible: Boolean,
block: (T) -> Unit