Simplify Polycentric profile export with file export option

- Remove GZIP compression logic that was causing crashes
- Revert to simple QR code generation with single error correction level
- Add file export button when QR code is too large for scanning
- Add FileProvider configuration for sharing exported files
- Update string resources for new file export functionality
- Keep fullscreen QR code viewer for smaller profiles

This provides a more reliable solution: QR codes work for smaller profiles,
and file export handles large profiles that exceed QR code limits.
This commit is contained in:
Trevor
2025-08-18 14:26:25 -05:00
parent c862b60c71
commit bc67f4c486
21 changed files with 86 additions and 105 deletions
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
size 65512557
+3 -1
View File
@@ -119,7 +119,7 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
signingConfig signingConfigs.debug
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@@ -231,4 +231,6 @@ dependencies {
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//ffmpeg
implementation files('libs/ffmpeg-kit-full-6.0-2.aar')
}
Binary file not shown.
@@ -14,8 +14,10 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
@@ -40,13 +42,13 @@ 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
import java.io.File
import java.io.FileWriter
class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _buttonShare: BigButton;
private lateinit var _buttonCopy: BigButton;
private lateinit var _buttonExportFile: BigButton;
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
@@ -64,6 +66,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
_buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy)
_buttonExportFile = findViewById(R.id.button_export_file)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr)
_textQRHint = findViewById(R.id.text_qr_hint)
@@ -78,6 +81,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
_buttonExportFile.visibility = View.INVISIBLE
lifecycleScope.launch {
try {
@@ -111,12 +115,14 @@ class PolycentricBackupActivity : AppCompatActivity() {
// 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)
// Show file export button when QR code is too large
if (e.message?.contains("Data too big") == true) {
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
_buttonExportFile.visibility = View.VISIBLE
} else {
_textQR.text = getString(R.string.failed_to_generate_qr_code)
}
_textQR.text = errorMessage
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.INVISIBLE
_buttonShare.visibility = View.VISIBLE
@@ -139,36 +145,19 @@ class PolycentricBackupActivity : AppCompatActivity() {
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
clipboard.setPrimaryClip(clip);
};
_buttonExportFile.onClick.subscribe {
exportToFile()
};
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
// 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
)
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
hints[EncodeHintType.MARGIN] = 1
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")
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
@@ -259,31 +248,38 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString())
.build();
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
val data = urlInfo.toByteArray()
return "polycentric://" + data.toBase64Url()
}
private fun compressData(data: ByteArray): ByteArray {
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(data)
private fun exportToFile() {
try {
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
val file = File(filesDir, fileName)
FileWriter(file).use { writer ->
writer.write(_exportBundle)
}
val uri = FileProvider.getUriForFile(
this,
"${packageName}.fileprovider",
file
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_SUBJECT, "Polycentric Profile Export")
putExtra(Intent.EXTRA_TEXT, "Polycentric profile export file")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, "Export Profile to File"))
} catch (e: Exception) {
Logger.e(TAG, "Failed to export to file", e)
UIDialogs.toast(this, "Failed to export profile to file")
}
return outputStream.toByteArray()
}
companion object {
@@ -30,8 +30,6 @@ 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;
@@ -110,19 +108,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
// 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}")
}
}
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
@@ -178,21 +164,6 @@ 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";
}
@@ -90,6 +90,15 @@
app:buttonSubText="@string/copy_your_identity_to_clipboard"
app:buttonIcon="@drawable/ic_copy"
android:layout_marginTop="8dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_export_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonText="@string/export_to_file"
app:buttonSubText="@string/save_profile_to_file_for_sharing"
app:buttonIcon="@drawable/ic_download"
android:layout_marginTop="8dp" />
</LinearLayout>
<ProgressBar
+4 -1
View File
@@ -642,8 +642,11 @@
<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="qr_code_too_large_use_file_export">QR code too large. Use the file export button below to share your profile.</string>
<string name="tap_qr_code_for_fullscreen">Tap QR code for fullscreen view</string>
<string name="export_to_file">Export to File</string>
<string name="save_profile_to_file_for_sharing">Save profile to file for sharing</string>
<string name="authority">com.futo.platformplayer.fileprovider</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>
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="shares" path="shares/"/>
<files-path name="files" path="." />
<external-path name="external_files" path="." />
</paths>
+3 -1
View File
@@ -6,7 +6,9 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
# Kotlin daemon memory settings
kotlin.daemon.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects