mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
export profile to file
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user