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.Store
import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo import userpackage.Protocol.URLInfo
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPOutputStream
import android.util.Base64
class PolycentricBackupActivity : AppCompatActivity() { class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _buttonShare: BigButton; private lateinit var _buttonShare: BigButton;
@@ -74,6 +79,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
try { try {
val pair = withContext(Dispatchers.IO) { val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle() val bundle = createExportBundle()
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
val dimension = TypedValue.applyDimension( val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt() ).toInt()
@@ -89,10 +96,22 @@ class PolycentricBackupActivity : AppCompatActivity() {
_buttonCopy.visibility = View.VISIBLE _buttonCopy.visibility = View.VISIBLE
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e) 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 _imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally { } finally {
_loader.visibility = View.GONE _loader.visibility = View.GONE
} }
@@ -111,8 +130,33 @@ class PolycentricBackupActivity : AppCompatActivity() {
} }
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap { private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height); // Try different error correction levels and settings to handle large data
return bitMatrixToBitmap(bitMatrix); 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 { private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
@@ -203,7 +247,31 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString()) .setBody(exportBundle.toByteString())
.build(); .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 { companion object {
@@ -30,6 +30,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
import java.io.ByteArrayInputStream
import java.util.zip.GZIPInputStream
class PolycentricImportProfileActivity : AppCompatActivity() { class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
@@ -108,7 +110,20 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val data = url.substring("polycentric://".length).base64UrlToByteArray(); 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) { if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle") 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 { companion object {
private const val TAG = "PolycentricImportProfileActivity"; private const val TAG = "PolycentricImportProfileActivity";
} }
+1
View File
@@ -404,6 +404,7 @@
<string name="unknown_reconstruction_type">Unbekannter Rekonstruktionstyp</string> <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_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="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="share_text">Text teilen</string>
<string name="copied_text">Text kopiert</string> <string name="copied_text">Text kopiert</string>
<string name="must_be_at_least_3_characters_long">Muss mindestens 3 Zeichen lang sein.</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="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_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="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="share_text">Compartir texto</string>
<string name="copied_text">Texto copiado</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> <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="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_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="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="share_text">Partager le texte</string>
<string name="copied_text">Texte copié</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> <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_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_parse_newpipe_subscriptions">Failed to parse NewPipe Subscriptions</string>
<string name="failed_to_generate_qr_code">Failed to generate QR code</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="share_text">Share Text</string>
<string name="copied_text">Copied 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> <string name="must_be_at_least_3_characters_long">Must be at least 3 characters long.</string>