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.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
import com.futo.polycentric.core.StorageTypeCRDTItem
|
||||||
@@ -39,18 +42,31 @@ 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;
|
||||||
private lateinit var _buttonCopy: BigButton;
|
private lateinit var _buttonCopy: BigButton;
|
||||||
|
private lateinit var _buttonExportFile: BigButton;
|
||||||
private lateinit var _imageQR: ImageView;
|
private lateinit var _imageQR: ImageView;
|
||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
|
private lateinit var _textQRHint: TextView;
|
||||||
private lateinit var _loader: View
|
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?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
@@ -62,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share)
|
_buttonShare = findViewById(R.id.button_share)
|
||||||
_buttonCopy = findViewById(R.id.button_copy)
|
_buttonCopy = findViewById(R.id.button_copy)
|
||||||
|
_buttonExportFile = findViewById(R.id.button_export_file)
|
||||||
_imageQR = findViewById(R.id.image_qr)
|
_imageQR = findViewById(R.id.image_qr)
|
||||||
_textQR = findViewById(R.id.text_qr)
|
_textQR = findViewById(R.id.text_qr)
|
||||||
|
_textQRHint = findViewById(R.id.text_qr_hint)
|
||||||
_loader = findViewById(R.id.progress_loader)
|
_loader = findViewById(R.id.progress_loader)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -71,16 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_imageQR.visibility = View.INVISIBLE
|
||||||
_textQR.visibility = View.INVISIBLE
|
_textQR.visibility = View.INVISIBLE
|
||||||
|
_textQRHint.visibility = View.INVISIBLE
|
||||||
_loader.visibility = View.VISIBLE
|
_loader.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
_buttonShare.visibility = View.INVISIBLE
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
_buttonCopy.visibility = View.INVISIBLE
|
||||||
|
_buttonExportFile.visibility = View.INVISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
|
||||||
|
_exportBundle = bundle
|
||||||
|
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val pair = withContext(Dispatchers.IO) {
|
val pair = withContext(Dispatchers.IO) {
|
||||||
val bundle = createExportBundle()
|
if (!isContentSuitableForQRCode(bundle)) {
|
||||||
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
val dimension = TypedValue.applyDimension(
|
val dimension = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||||
).toInt()
|
).toInt()
|
||||||
@@ -88,28 +113,33 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
Pair(bundle, qr)
|
Pair(bundle, qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
_exportBundle = pair.first
|
|
||||||
_imageQR.setImageBitmap(pair.second)
|
_imageQR.setImageBitmap(pair.second)
|
||||||
_imageQR.visibility = View.VISIBLE
|
_imageQR.visibility = View.VISIBLE
|
||||||
_textQR.visibility = View.VISIBLE
|
_textQR.visibility = View.VISIBLE
|
||||||
|
_textQRHint.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.VISIBLE
|
_buttonShare.visibility = View.VISIBLE
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
_buttonCopy.visibility = View.VISIBLE
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
_imageQR.setOnClickListener {
|
||||||
|
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
|
||||||
// Show the export bundle text even if QR code generation fails
|
startActivity(intent)
|
||||||
_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
|
} 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
|
_textQR.visibility = View.VISIBLE
|
||||||
|
_textQRHint.visibility = View.INVISIBLE
|
||||||
_buttonShare.visibility = View.VISIBLE
|
_buttonShare.visibility = View.VISIBLE
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
_buttonCopy.visibility = View.VISIBLE
|
||||||
|
|
||||||
// Hide QR image since generation failed
|
// Hide QR image since generation failed
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_imageQR.visibility = View.INVISIBLE
|
||||||
} finally {
|
} finally {
|
||||||
@@ -127,36 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
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 {
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
// Try different error correction levels and settings to handle large data
|
if (!isContentSuitableForQRCode(content)) {
|
||||||
val errorCorrectionLevels = listOf(
|
throw Exception("Data too big for QR code generation")
|
||||||
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
|
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||||
throw lastException ?: Exception("Failed to generate QR code")
|
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
||||||
|
hints[EncodeHintType.MARGIN] = 1
|
||||||
|
|
||||||
|
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||||||
|
return bitMatrixToBitmap(bitMatrix)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
@@ -247,31 +270,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
.setBody(exportBundle.toByteString())
|
.setBody(exportBundle.toByteString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
val originalData = urlInfo.toByteArray()
|
val data = urlInfo.toByteArray()
|
||||||
val originalUrl = "polycentric://" + originalData.toBase64Url()
|
return "polycentric://" + data.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 {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
android:id="@+id/image_qr"
|
android:id="@+id/image_qr"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
android:layout_height="200dp"
|
android:layout_height="200dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
app:srcCompat="@drawable/ic_qr"
|
app:srcCompat="@drawable/ic_qr"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
@@ -37,15 +39,31 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_qr"
|
android:id="@+id/text_qr"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/scan_to_import"
|
android:text="@string/scan_to_import"
|
||||||
android:fontFamily="@font/inter_light"
|
android:fontFamily="@font/inter_light"
|
||||||
android:textSize="32dp"
|
android:textSize="32dp"
|
||||||
|
android:textAlignment="center"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/image_qr"
|
app:layout_constraintTop_toBottomOf="@id/image_qr"
|
||||||
app:layout_constraintLeft_toLeftOf="@id/image_qr"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="@id/image_qr" />
|
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
|
<LinearLayout
|
||||||
android:id="@+id/layout_buttons"
|
android:id="@+id/layout_buttons"
|
||||||
@@ -55,7 +73,7 @@
|
|||||||
android:layout_marginStart="20dp"
|
android:layout_marginStart="20dp"
|
||||||
android:layout_marginEnd="20dp"
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginTop="30dp"
|
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_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent">
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
@@ -75,6 +93,15 @@
|
|||||||
app:buttonSubText="@string/copy_your_identity_to_clipboard"
|
app:buttonSubText="@string/copy_your_identity_to_clipboard"
|
||||||
app:buttonIcon="@drawable/ic_copy"
|
app:buttonIcon="@drawable/ic_copy"
|
||||||
android:layout_marginTop="8dp" />
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -1176,4 +1176,9 @@
|
|||||||
<item>1500</item>
|
<item>1500</item>
|
||||||
<item>2000</item>
|
<item>2000</item>
|
||||||
</string-array>
|
</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user