export profile to file

This commit is contained in:
austin
2025-11-13 18:22:58 -06:00
parent 9e9d26c752
commit e4f51bb130
3 changed files with 106 additions and 74 deletions
@@ -13,15 +13,18 @@ import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
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
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.StorageTypeCRDTItem
@@ -39,18 +42,31 @@ 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;
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;
private lateinit var _textQRHint: TextView;
private lateinit var _loader: View
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
uri?.let { fileUri ->
try {
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
outputStream.write(_exportBundle.toByteArray())
}
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
} catch (e: Exception) {
Logger.e(TAG, "Failed to write to document", e)
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
@@ -62,8 +78,10 @@ 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)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
@@ -71,15 +89,22 @@ class PolycentricBackupActivity : AppCompatActivity() {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_textQRHint.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
_buttonExportFile.visibility = View.INVISIBLE
lifecycleScope.launch {
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
_exportBundle = bundle
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
if (!isContentSuitableForQRCode(bundle)) {
throw Exception("Data too big for QR code generation")
}
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
@@ -88,25 +113,30 @@ class PolycentricBackupActivity : AppCompatActivity() {
Pair(bundle, qr)
}
_exportBundle = pair.first
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_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)
_imageQR.setOnClickListener {
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
startActivity(intent)
}
_textQR.text = errorMessage
} catch (e: Exception) {
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
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.visibility = View.VISIBLE
_textQRHint.visibility = View.INVISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
@@ -127,36 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
clipboard.setPrimaryClip(clip);
};
_buttonExportFile.onClick.subscribe {
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
_createDocumentLauncher.launch(fileName)
};
}
private fun isContentSuitableForQRCode(content: String): Boolean {
val bytes = content.toByteArray(Charsets.UTF_8)
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
}
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
)
if (!isContentSuitableForQRCode(content)) {
throw Exception("Data too big for QR code generation")
}
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.ERROR_CORRECTION] = ErrorCorrectionLevel.M
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 {
@@ -247,31 +270,8 @@ 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
}
private fun compressData(data: ByteArray): ByteArray {
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(data)
}
return outputStream.toByteArray()
val data = urlInfo.toByteArray()
return "polycentric://" + data.toBase64Url()
}
companion object {
@@ -29,6 +29,8 @@
android:id="@+id/image_qr"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp"
app:srcCompat="@drawable/ic_qr"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@@ -37,15 +39,31 @@
<TextView
android:id="@+id/text_qr"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/scan_to_import"
android:fontFamily="@font/inter_light"
android:textSize="32dp"
android:textAlignment="center"
android:layout_marginTop="12dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/image_qr"
app:layout_constraintLeft_toLeftOf="@id/image_qr"
app:layout_constraintRight_toRightOf="@id/image_qr" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/text_qr_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tap_qr_code_for_fullscreen"
android:fontFamily="@font/inter_light"
android:textSize="14dp"
android:textColor="@color/gray_400"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/text_qr"
app:layout_constraintLeft_toLeftOf="@id/text_qr"
app:layout_constraintRight_toRightOf="@id/text_qr" />
<LinearLayout
android:id="@+id/layout_buttons"
@@ -55,7 +73,7 @@
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/text_qr"
app:layout_constraintTop_toBottomOf="@id/text_qr_hint"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
@@ -75,6 +93,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
+5
View File
@@ -1176,4 +1176,9 @@
<item>1500</item>
<item>2000</item>
</string-array>
<string name="qr_code_too_large_use_file_export">Export to file or copy backup code</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="profile_saved_successfully">Profile saved successfully</string>
</resources>