Fix QR code generation for large polycentric export bundles

- Add GZIP compression for large export data (>2000 chars)
- Implement fallback QR generation with different error correction levels
- Add automatic decompression support in import functionality
- Improve error handling with fallback to text display
- Add localized error messages for QR code failures
- Add compression ratio logging for debugging

This fixes the 'Data too big' error when generating QR codes for
polycentric profile exports by automatically compressing large data
and providing multiple fallback mechanisms.
This commit is contained in:
Trevor
2025-08-13 11:34:56 -05:00
parent 31a34e4583
commit 4b3e89d0af
6 changed files with 109 additions and 7 deletions
@@ -29,14 +29,19 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPOutputStream
import android.util.Base64
class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _buttonShare: BigButton;
@@ -74,6 +79,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
@@ -89,10 +96,22 @@ class PolycentricBackupActivity : AppCompatActivity() {
_buttonCopy.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
// Show the export bundle text even if QR code generation fails
_exportBundle = withContext(Dispatchers.IO) { createExportBundle() }
// Provide more specific error message based on the exception
val errorMessage = when {
e.message?.contains("Data too big") == true -> getString(R.string.qr_code_too_large_use_text_below)
else -> getString(R.string.failed_to_generate_qr_code)
}
_textQR.text = errorMessage
_textQR.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
// Hide QR image since generation failed
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
@@ -111,8 +130,33 @@ class PolycentricBackupActivity : AppCompatActivity() {
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
// Try different error correction levels and settings to handle large data
val errorCorrectionLevels = listOf(
ErrorCorrectionLevel.L, // 7% recovery
ErrorCorrectionLevel.M, // 15% recovery
ErrorCorrectionLevel.Q, // 25% recovery
ErrorCorrectionLevel.H // 30% recovery
)
var lastException: Exception? = null
for (errorLevel in errorCorrectionLevels) {
try {
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = errorLevel
hints[EncodeHintType.MARGIN] = 1
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
} catch (e: Exception) {
lastException = e
Logger.w(TAG, "Failed to generate QR code with error correction level $errorLevel: ${e.message}")
continue
}
}
// If all attempts fail, throw the last exception
throw lastException ?: Exception("Failed to generate QR code")
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
@@ -203,7 +247,31 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString())
.build();
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
val originalData = urlInfo.toByteArray()
val originalUrl = "polycentric://" + originalData.toBase64Url()
// If the original URL is too long, try compression
if (originalUrl.length > 2000) { // QR code practical limit
try {
val compressedData = compressData(originalData)
val compressedUrl = "polycentric://" + compressedData.toBase64Url()
val compressionRatio = (compressedUrl.length.toFloat() / originalUrl.length * 100).toInt()
Logger.i(TAG, "Using compressed export bundle. Original size: ${originalUrl.length}, Compressed size: ${compressedUrl.length}, Compression ratio: ${compressionRatio}%")
return compressedUrl
} catch (e: Exception) {
Logger.w(TAG, "Failed to compress export bundle, using original", e)
}
}
return originalUrl
}
private fun compressData(data: ByteArray): ByteArray {
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(data)
}
return outputStream.toByteArray()
}
companion object {
@@ -30,6 +30,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import java.io.ByteArrayInputStream
import java.util.zip.GZIPInputStream
class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
@@ -108,7 +110,20 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
// Try to parse as regular data first, if it fails, try decompressing
val urlInfo = try {
Protocol.URLInfo.parseFrom(data)
} catch (e: Exception) {
// If parsing fails, try to decompress the data
try {
val decompressedData = decompressData(data)
Protocol.URLInfo.parseFrom(decompressedData)
} catch (decompressException: Exception) {
throw Exception("Failed to parse URL data: ${e.message}")
}
}
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
@@ -163,6 +178,21 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
}
private fun decompressData(data: ByteArray): ByteArray {
val inputStream = ByteArrayInputStream(data)
val outputStream = java.io.ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(8192) // 8KB buffer
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}
companion object {
private const val TAG = "PolycentricImportProfileActivity";
}
+1
View File
@@ -404,6 +404,7 @@
<string name="unknown_reconstruction_type">Unbekannter Rekonstruktionstyp</string>
<string name="failed_to_parse_newpipe_subscriptions">Fehler beim Parsen von NewPipe-Abonnements</string>
<string name="failed_to_generate_qr_code">Fehler beim Generieren des QR-Codes</string>
<string name="qr_code_too_large_use_text_below">QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen.</string>
<string name="share_text">Text teilen</string>
<string name="copied_text">Text kopiert</string>
<string name="must_be_at_least_3_characters_long">Muss mindestens 3 Zeichen lang sein.</string>
+1
View File
@@ -381,6 +381,7 @@
<string name="unknown_reconstruction_type">Tipo de reconstrucción desconocido</string>
<string name="failed_to_parse_newpipe_subscriptions">Error al analizar las suscripciones de NewPipe</string>
<string name="failed_to_generate_qr_code">Error al generar el código QR</string>
<string name="qr_code_too_large_use_text_below">Código QR demasiado grande. Use el texto de abajo para compartir su perfil.</string>
<string name="share_text">Compartir texto</string>
<string name="copied_text">Texto copiado</string>
<string name="must_be_at_least_3_characters_long">Debe tener al menos 3 caracteres de longitud.</string>
+1
View File
@@ -420,6 +420,7 @@
<string name="unknown_reconstruction_type">Type de reconstruction inconnu</string>
<string name="failed_to_parse_newpipe_subscriptions">Échec de l\'analyse des abonnements NewPipe</string>
<string name="failed_to_generate_qr_code">Échec de la génération du code QR</string>
<string name="qr_code_too_large_use_text_below">Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil.</string>
<string name="share_text">Partager le texte</string>
<string name="copied_text">Texte copié</string>
<string name="must_be_at_least_3_characters_long">Doit comporter au moins 3 caractères.</string>
+1
View File
@@ -642,6 +642,7 @@
<string name="failed_to_parse_text_file">Failed to parse text file</string>
<string name="failed_to_parse_newpipe_subscriptions">Failed to parse NewPipe Subscriptions</string>
<string name="failed_to_generate_qr_code">Failed to generate QR code</string>
<string name="qr_code_too_large_use_text_below">QR code too large. Use the text below to share your profile.</string>
<string name="share_text">Share Text</string>
<string name="copied_text">Copied Text</string>
<string name="must_be_at_least_3_characters_long">Must be at least 3 characters long.</string>