diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d271724b..2823496c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -245,5 +245,9 @@
android:name=".activities.PolycentricModerationActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
index 9cf58134..963999ce 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
@@ -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
@@ -29,8 +32,10 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo
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))
}
@@ -57,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(R.id.button_back).setOnClickListener {
finish();
@@ -66,14 +89,23 @@ 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()
+ if (!isContentSuitableForQRCode(bundle)) {
+ throw Exception("Data too big for QR code generation")
+ }
+
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
@@ -81,18 +113,35 @@ 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
+
+ _imageQR.setOnClickListener {
+ val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
+ startActivity(intent)
+ }
} catch (e: Exception) {
- Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
+ 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
+
+ // Hide QR image since generation failed
_imageQR.visibility = View.INVISIBLE
- _textQR.visibility = View.INVISIBLE
- _buttonShare.visibility = View.INVISIBLE
- _buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
@@ -108,11 +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 {
- val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
- return bitMatrixToBitmap(bitMatrix);
+ if (!isContentSuitableForQRCode(content)) {
+ throw Exception("Data too big for QR code generation")
+ }
+
+ val hints = java.util.EnumMap(EncodeHintType::class.java)
+ 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 {
@@ -203,7 +270,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString())
.build();
- return "polycentric://" + urlInfo.toByteArray().toBase64Url()
+ val data = urlInfo.toByteArray()
+ return "polycentric://" + data.toBase64Url()
}
companion object {
diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt
index ab6d70a3..91a0cb5c 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt
@@ -32,100 +32,166 @@ import userpackage.Protocol
import userpackage.Protocol.ExportBundle
class PolycentricImportProfileActivity : AppCompatActivity() {
- private lateinit var _buttonHelp: ImageButton;
- private lateinit var _buttonScanProfile: LinearLayout;
- private lateinit var _buttonImportProfile: LinearLayout;
- private lateinit var _editProfile: EditText;
- private lateinit var _loaderOverlay: LoaderOverlay;
+ private lateinit var _buttonHelp: ImageButton
+ private lateinit var _buttonScanProfile: LinearLayout
+ private lateinit var _buttonImportFile: LinearLayout
+ private lateinit var _buttonImportProfile: LinearLayout
+ private lateinit var _editProfile: EditText
+ private lateinit var _loaderOverlay: LoaderOverlay
- private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
- scanResult?.let {
- if (it.contents != null) {
- val scannedUrl = it.contents
- import(scannedUrl)
+ private val _qrCodeResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ val scanResult =
+ IntentIntegrator.parseActivityResult(result.resultCode, result.data)
+ scanResult?.let {
+ if (it.contents != null) {
+ val scannedUrl = it.contents
+ import(scannedUrl)
+ }
+ }
+ }
+
+ private val _filePickerLauncher =
+ registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ uri?.let { fileUri ->
+ try {
+ // Check file size before reading
+ val fileSize =
+ contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
+ val maxFileSize = 10 * 1024 * 1024 // 10MB limit
+
+ if (fileSize > maxFileSize) {
+ UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
+ return@let
+ }
+
+ if (fileSize == 0L) {
+ UIDialogs.toast(this, "Selected file is empty.")
+ return@let
+ }
+
+ val content =
+ contentResolver
+ .openInputStream(fileUri)
+ ?.bufferedReader()
+ ?.readText()
+ content?.let { fileContent ->
+ val trimmedContent = fileContent.trim()
+
+ // Check if content is empty after trimming
+ if (trimmedContent.isEmpty()) {
+ UIDialogs.toast(this, "Selected file contains no data.")
+ return@let
+ }
+
+ // Check if content looks like a valid polycentric URL
+ if (!trimmedContent.startsWith("polycentric://")) {
+ UIDialogs.toast(
+ this,
+ "Selected file does not contain a valid polycentric profile URL."
+ )
+ return@let
+ }
+
+ import(trimmedContent)
+ }
+ ?: run { UIDialogs.toast(this, "Could not read file content.") }
+ } catch (e: SecurityException) {
+ Logger.e(TAG, "Security exception reading file", e)
+ UIDialogs.toast(this, "Permission denied to read file.")
+ } catch (e: OutOfMemoryError) {
+ Logger.e(TAG, "Out of memory reading file", e)
+ UIDialogs.toast(this, "File too large to process.")
+ } catch (e: Exception) {
+ Logger.e(TAG, "Failed to read file", e)
+ UIDialogs.toast(this, "Failed to read file: ${e.message}")
+ }
+ }
}
- }
- }
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_polycentric_import_profile);
- setNavigationBarColorAndIcons();
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_polycentric_import_profile)
+ setNavigationBarColorAndIcons()
- _buttonHelp = findViewById(R.id.button_help);
- _buttonScanProfile = findViewById(R.id.button_scan_profile);
- _buttonImportProfile = findViewById(R.id.button_import_profile);
- _loaderOverlay = findViewById(R.id.loader_overlay);
- _editProfile = findViewById(R.id.edit_profile);
- findViewById(R.id.button_back).setOnClickListener {
- finish();
- };
+ _buttonHelp = findViewById(R.id.button_help)
+ _buttonScanProfile = findViewById(R.id.button_scan_profile)
+ _buttonImportFile = findViewById(R.id.button_import_file)
+ _buttonImportProfile = findViewById(R.id.button_import_profile)
+ _loaderOverlay = findViewById(R.id.loader_overlay)
+ _editProfile = findViewById(R.id.edit_profile)
+ findViewById(R.id.button_back).setOnClickListener { finish() }
_buttonHelp.setOnClickListener {
- startActivity(Intent(this, PolycentricWhyActivity::class.java));
- };
+ startActivity(Intent(this, PolycentricWhyActivity::class.java))
+ }
_buttonScanProfile.setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
- integrator.setOrientationLocked(true);
+ integrator.setOrientationLocked(true)
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true)
- integrator.setCaptureActivity(QRCaptureActivity::class.java);
+ integrator.setCaptureActivity(QRCaptureActivity::class.java)
_qrCodeResultLauncher.launch(integrator.createScanIntent())
- };
+ }
+
+ _buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") }
_buttonImportProfile.setOnClickListener {
if (_editProfile.text.isEmpty()) {
- UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
- return@setOnClickListener;
+ UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data))
+ return@setOnClickListener
}
- import(_editProfile.text.toString());
- };
+ import(_editProfile.text.toString())
+ }
- val url = intent.getStringExtra("url");
+ val url = intent.getStringExtra("url")
if (url != null) {
- import(url);
+ import(url)
}
}
private fun import(url: String) {
if (!url.startsWith("polycentric://")) {
- UIDialogs.toast(this, getString(R.string.not_a_valid_url));
- return;
+ UIDialogs.toast(this, getString(R.string.not_a_valid_url))
+ return
}
_loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
- val data = url.substring("polycentric://".length).base64UrlToByteArray();
- val urlInfo = Protocol.URLInfo.parseFrom(data);
+ val data = url.substring("polycentric://".length).base64UrlToByteArray()
+ val urlInfo = Protocol.URLInfo.parseFrom(data)
+
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
- val exportBundle = ExportBundle.parseFrom(urlInfo.body);
- val keyPair = KeyPair.fromProto(exportBundle.keyPair);
+ val exportBundle = ExportBundle.parseFrom(urlInfo.body)
+ val keyPair = KeyPair.fromProto(exportBundle.keyPair)
- val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
+ val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey)
if (existingProcessSecret != null) {
withContext(Dispatchers.Main) {
- UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
+ UIDialogs.toast(
+ this@PolycentricImportProfileActivity,
+ getString(R.string.this_profile_is_already_imported)
+ )
}
- return@launch;
+ return@launch
}
- val processSecret = ProcessSecret(keyPair, Process.random());
- Store.instance.addProcessSecret(processSecret);
+ val processSecret = ProcessSecret(keyPair, Process.random())
+ Store.instance.addProcessSecret(processSecret)
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
@@ -133,37 +199,43 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
- val processHandle = processSecret.toProcessHandle();
+ val processHandle = processSecret.toProcessHandle()
for (e in exportBundle.events.eventsList) {
try {
- val se = SignedEvent.fromProto(e);
- Store.instance.putSignedEvent(se);
+ val se = SignedEvent.fromProto(e)
+ Store.instance.putSignedEvent(se)
} catch (e: Throwable) {
- Logger.w(TAG, "Ignored invalid event", e);
+ Logger.w(TAG, "Ignored invalid event", e)
}
}
- StatePolycentric.instance.setProcessHandle(processHandle);
- processHandle.fullyBackfillClient(ApiMethods.SERVER);
+ StatePolycentric.instance.setProcessHandle(processHandle)
+ processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) {
- startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
- finish();
+ startActivity(
+ Intent(
+ this@PolycentricImportProfileActivity,
+ PolycentricProfileActivity::class.java
+ )
+ )
+ finish()
}
} catch (e: Throwable) {
- Logger.w(TAG, "Failed to import profile", e);
+ Logger.w(TAG, "Failed to import profile", e)
withContext(Dispatchers.Main) {
- UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
+ UIDialogs.toast(
+ this@PolycentricImportProfileActivity,
+ getString(R.string.failed_to_import_profile) + " '${e.message}'"
+ )
}
} finally {
- withContext(Dispatchers.Main) {
- _loaderOverlay.hide();
- }
+ withContext(Dispatchers.Main) { _loaderOverlay.hide() }
}
}
}
companion object {
- private const val TAG = "PolycentricImportProfileActivity";
+ private const val TAG = "PolycentricImportProfileActivity"
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt
new file mode 100644
index 00000000..9acefed2
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt
@@ -0,0 +1,109 @@
+package com.futo.platformplayer.activities
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.os.Bundle
+import android.util.TypedValue
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.appcompat.app.AppCompatActivity
+import com.futo.platformplayer.R
+import com.futo.platformplayer.setNavigationBarColorAndIcons
+import com.futo.platformplayer.states.StateApp
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
+import com.google.zxing.MultiFormatWriter
+import com.google.zxing.common.BitMatrix
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
+
+class QRCodeFullscreenActivity : AppCompatActivity() {
+ companion object {
+ private const val EXTRA_QR_TEXT = "qr_text"
+
+ fun createIntent(context: Context, qrText: String): android.content.Intent {
+ return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
+ putExtra(EXTRA_QR_TEXT, qrText)
+ }
+ }
+ }
+
+ override fun attachBaseContext(newBase: Context?) {
+ super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_qr_code_fullscreen)
+ setNavigationBarColorAndIcons()
+
+ val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
+
+ val imageQR = findViewById(R.id.image_qr_fullscreen)
+ val buttonBack = findViewById(R.id.button_back_fullscreen)
+ val buttonClose = findViewById(R.id.button_close_fullscreen)
+
+ // Generate QR code bitmap from text
+ qrText?.let { text ->
+ try {
+ if (!isContentSuitableForQRCode(text)) {
+ throw Exception("Data too big for QR code generation")
+ }
+
+ val dimension = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
+ ).toInt()
+ val qrBitmap = generateQRCode(text, dimension, dimension)
+ imageQR.setImageBitmap(qrBitmap)
+ } catch (e: Exception) {
+ // If QR generation fails, show error or fallback
+ imageQR.setImageResource(R.drawable.ic_qr)
+ }
+ }
+
+ buttonBack.setOnClickListener {
+ finish()
+ }
+
+ buttonClose.setOnClickListener {
+ finish()
+ }
+
+ imageQR.setOnClickListener {
+ finish()
+ }
+ }
+
+ 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 {
+ if (!isContentSuitableForQRCode(content)) {
+ throw Exception("Data too big for QR code generation")
+ }
+
+ val hints = java.util.EnumMap(EncodeHintType::class.java)
+ 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 {
+ val width = matrix.width
+ val height = matrix.height
+ val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE)
+ }
+ }
+ return bmp
+ }
+}
+
diff --git a/app/src/main/res/layout/activity_polycentric_backup.xml b/app/src/main/res/layout/activity_polycentric_backup.xml
index d6579dd7..130c4934 100644
--- a/app/src/main/res/layout/activity_polycentric_backup.xml
+++ b/app/src/main/res/layout/activity_polycentric_backup.xml
@@ -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 @@
+ 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" />
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_qr_code_fullscreen.xml b/app/src/main/res/layout/activity_qr_code_fullscreen.xml
new file mode 100644
index 00000000..0dd128e2
--- /dev/null
+++ b/app/src/main/res/layout/activity_qr_code_fullscreen.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 4724f6ce..269736f8 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -388,6 +388,7 @@
استثناء غير معالج في VS
إرسال الاستثناء للمطورين…
تم تعيين مفتاح الترخيص الخاص بك!\nقد يكون هناك حاجة لإعادة تشغيل التطبيق.
+ رمز الاستجابة السريعة كبير جدًا. استخدم النص أدناه لمشاركة ملفك الشخصي.
تنسيق الترخيص غير صالح
تنسيق المحتوى غير معروف
تنسيق الملف غير معروف
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 234bb900..7cd81816 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -395,6 +395,7 @@
Unbehandelte Ausnahme in VS
Ausnahme an Entwickler senden…
Ihr Lizenzschlüssel wurde festgelegt!\nEin Neustart der App könnte erforderlich sein.
+ QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen.
Ungültiges Lizenzformat
Unbekanntes Inhaltsformat
Unbekanntes Dateiformat
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 96b9449a..e9c04d20 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -372,6 +372,7 @@
Excepción no manejada en VS
Enviar excepción a los desarrolladores...
¡Se ha configurado tu clave de licencia!\nPuede ser necesario reiniciar la aplicación.
+ Código QR demasiado grande. Use el texto de abajo para compartir su perfil.
Formato de licencia no válido
Formato de contenido desconocido
Formato de archivo desconocido
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 557c485f..48ffdafe 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -411,6 +411,7 @@
Exception non gérée dans VS
Envoyer l\'exception aux développeurs…
Votre clé de licence a été définie !\nUn redémarrage de l\'application peut être nécessaire.
+ Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil.
Format de licence invalide
Format de contenu inconnu
Format de fichier inconnu
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index a5783903..d91c3b0b 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -617,6 +617,7 @@
Eccezione non gestita in VS
Invio eccezione agli sviluppatori…
La tua chiave di licenza è stata impostata!\nIl riavvio dell\'app potrebbe essere richiesto.
+ Codice QR troppo grande. Usa il testo qui sotto per condividere il tuo profilo.
Formato licenza non valido
Formato contenuto sconosciuto
Formato file sconosciuto
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index e3452959..9177ffac 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -374,6 +374,7 @@
VSで未処理の例外
開発者に例外を送信…
ライセンスキーが設定されました!\nアプリを再起動する可能性があります。
+ QRコードが大きすぎます。下のテキストを使用してプロフィールを共有してください。
無効なライセンス形式
不明なコンテンツ形式
不明なファイル形式
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 0312da2f..01a84b8d 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -410,6 +410,7 @@
VS에서 처리되지 않은 예외
개발자에게 예외를 보냅니다…
라이선스 키가 설정되었습니다!\n앱을 다시 시작해야 할 수 있습니다.
+ QR 코드가 너무 큽니다. 아래 텍스트를 사용하여 프로필을 공유하세요.
잘못된 라이선스 형식
알 수 없는 콘텐츠 형식
알 수 없는 파일 형식
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index de46d305..9a0fcec9 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -407,6 +407,7 @@
Exceção não tratada no VS
Enviar exceção aos desenvolvedores…
Sua chave de licença foi definida!\nUma reinicialização do aplicativo pode ser necessária.
+ Código QR muito grande. Use o texto abaixo para compartilhar seu perfil.
Formato de licença inválido
Formato de conteúdo desconhecido
Formato de arquivo desconhecido
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 82a479ad..cf66c21d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -407,6 +407,7 @@
Необработанное исключение в VS
Отправить исключение разработчикам…
Ваш лицензионный ключ установлен!\nМожет потребоваться перезагрузка приложения.
+ QR-код слишком большой. Используйте текст ниже, чтобы поделиться своим профилем.
Неверный формат лицензии
Неизвестный формат содержимого
Неизвестный формат файла
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 5a17d7b9..8cc01b87 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -581,6 +581,7 @@
VS\'de bilinmeyen hata (exception)
Geliştiricilere exception\'ı gönder…
Lisans anahtarınız ayarlandı!\nUygulamayı yeniden başlatmanız gerekebilir.
+ QR kodu çok büyük. Profilinizi paylaşmak için aşağıdaki metni kullanın.
Geçersiz lisans formatı
Bilinmeyen içerik formatı
Bilinmeyen dosya formatı
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 1e0e843f..74ab2f65 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -411,6 +411,7 @@
VS 中的未处理异常
向开发者发送异常…
您的许可证密钥已设置!\n可能需要重新启动应用程序。
+ 二维码太大。请使用下方文字分享您的个人资料。
无效的许可证格式
未知的内容格式
未知的文件格式
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0f03e8f..7e0c60da 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -646,6 +646,7 @@
Unhandled exception in VS
Send exception to developers…
Your license key has been set!\nAn app restart might be required.
+ QR code too large. Use the text below to share your profile.
Invalid license format
Unknown content format
Unknown file format
@@ -1175,4 +1176,10 @@
- 1500
- 2000
+ Export to file or copy backup code
+ Tap QR code for fullscreen view
+ Export to File
+ Import from File
+ Save profile to file for sharing
+ Profile saved successfully