diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 536a774c..f8c9c743 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -406,6 +406,33 @@ class Settings : FragmentedStorageFileJson() { } + @FormField("External Storage", FieldForm.GROUP, "", 12) + var storage = Storage(); + @Serializable + class Storage { + var storage_general: String? = null; + var storage_download: String? = null; + + fun getStorageGeneralUri(): Uri? = storage_general?.let { Uri.parse(it) }; + fun getStorageDownloadUri(): Uri? = storage_download?.let { Uri.parse(it) }; + fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri()); + fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri()); + + @FormField("Change external General directory", FieldForm.BUTTON, "Change the external directory for general files, used for persistent files like auto-backup", 3) + fun changeStorageGeneral() { + SettingsActivity.getActivity()?.let { + StateApp.instance.changeExternalGeneralDirectory(it); + } + } + @FormField("Change external Downloads directory", FieldForm.BUTTON, "Change the external storage for download files, used for exported download files", 4) + fun changeStorageDownload() { + SettingsActivity.getActivity()?.let { + StateApp.instance.changeExternalDownloadDirectory(it); + } + } + } + + @FormField("Auto Update", "group", "Configure the auto updater", 12) var autoUpdate = AutoUpdate(); @Serializable @@ -511,7 +538,9 @@ class Settings : FragmentedStorageFileJson() { @FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1) fun configureAutomaticBackup() { - UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!); + UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) { + SettingsActivity.getActivity()?.reloadSettings(); + }; } @FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2) fun restoreAutomaticBackup() { @@ -542,6 +571,7 @@ class Settings : FragmentedStorageFileJson() { StatePayment.instance.clearLicenses(); SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Licenses cleared, might require app restart"); + it.reloadSettings(); } } } diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt index fffd4515..e6874619 100644 --- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt +++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt @@ -15,7 +15,9 @@ import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.dialogs.* import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.stores.v2.ManagedStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -90,11 +92,25 @@ class UIDialogs { } - fun showAutomaticBackupDialog(context: Context) { - val dialog = AutomaticBackupDialog(context); - registerDialogOpened(dialog); - dialog.setOnDismissListener { registerDialogClosed(dialog) }; - dialog.show(); + fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) { + val dialogAction: ()->Unit = { + val dialog = AutomaticBackupDialog(context); + registerDialogOpened(dialog); + dialog.setOnDismissListener { registerDialogClosed(dialog); onClosed?.invoke() }; + dialog.show(); + }; + if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck) + UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0, + UIDialogs.Action("Cancel", {}), //To nothing + UIDialogs.Action("Override", { + dialogAction(); + }, UIDialogs.ActionStyle.DANGEROUS), + UIDialogs.Action("Restore", { + UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope); + }, UIDialogs.ActionStyle.PRIMARY)); + else { + dialogAction(); + } } fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) { val dialog = AutomaticRestoreDialog(context, scope); @@ -134,10 +150,10 @@ class UIDialogs { val buttonView = TextView(context); val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt(); val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt(); - val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics); + val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt(); buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { if(actions.size > 1) - this.marginEnd = dp28; + this.marginEnd = if(actions.size > 2) dp14 else dp28; }; buttonView.setTextColor(Color.WHITE); buttonView.textSize = 14f; @@ -151,8 +167,9 @@ class UIDialogs { ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red)) else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary)) } + val paddingSpecialButtons = if(actions.size > 2) dp14 else dp28; if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT) - buttonView.setPadding(dp28, dp10, dp28, dp10); + buttonView.setPadding(paddingSpecialButtons, dp10, paddingSpecialButtons, dp10); else buttonView.setPadding(dp10, dp10, dp10, dp10); diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index b2ca4f80..b5ad24e3 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.icu.util.Output import android.os.Build import android.os.Looper import android.os.OperationCanceledException @@ -15,6 +16,7 @@ import android.view.WindowInsetsController import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.PlatformMultiClientPool @@ -75,6 +77,14 @@ fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPool fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec); +fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri); +fun DocumentFile.getOutputStream(context: Context, using: ((OutputStream?)->Unit)? = null) = context.contentResolver.openOutputStream(this.uri); +fun DocumentFile.copyTo(context: Context, file: DocumentFile) = this.getInputStream(context).use { input -> file.getOutputStream(context)?.let { output -> input?.copyTo(output) } }; +fun DocumentFile.readBytes(context: Context) = this.getInputStream(context).use { input -> input?.readBytes() }; +fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.contentResolver.openOutputStream(this.uri)?.use { + it.write(byteArray); + it.flush(); +}; fun loadBitmap(url: String): Bitmap { try { diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt index 29a8a3c2..93e6a330 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt @@ -11,6 +11,7 @@ import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup import com.google.android.material.button.MaterialButton @@ -58,13 +59,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) { } clearFocus(); dismiss(); + Logger.i(TAG, "Set AutoBackupPassword"); Settings.instance.backup.autoBackupPassword = _editPassword.text.toString(); Settings.instance.backup.didAskAutoBackup = true; Settings.instance.save(); UIDialogs.toast(context, "AutoBackup enabled"); - try { StateBackup.startAutomaticBackup(true); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index dd61fd79..27d5e63b 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -52,10 +52,9 @@ import java.util.concurrent.TimeUnit class StateApp { val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active + /* private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay"); - - fun getExternalRootDirectory(): File? { if(!externalRootDirectory.exists()) { val result = externalRootDirectory.mkdirs(); @@ -65,6 +64,57 @@ class StateApp { } else return externalRootDirectory; + }*/ + + fun getExternalGeneralDirectory(context: Context): DocumentFile? { + val generalUri = Settings.instance.storage.getStorageGeneralUri(); + if(isValidStorageUri(context, generalUri)) + return DocumentFile.fromTreeUri(context, generalUri!!); + return null; + } + fun changeExternalGeneralDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) { + if(context is Context) + requestDirectoryAccess(context, "General Files", "This directory is used to save auto-backups and other persistent files.", null) { + if(it != null) + context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)); + if(it != null && isValidStorageUri(context, it)) { + Logger.i(TAG, "Changed external general directory: ${it}"); + Settings.instance.storage.storage_general = it.toString(); + Settings.instance.save(); + + onChanged?.invoke(getExternalGeneralDirectory(context)); + } + else + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + UIDialogs.toast("Failed to gain access to\n [${it?.lastPathSegment}]"); + }; + }; + } + fun getExternalDownloadDirectory(context: Context): DocumentFile? { + val downloadUri = Settings.instance.storage.storage_download?.let { Uri.parse(it) }; + if(isValidStorageUri(context, downloadUri)) + return DocumentFile.fromTreeUri(context, downloadUri!!); + return null; + } + fun changeExternalDownloadDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) { + if(context is Context) + requestDirectoryAccess(context, "Download Exports", "This directory is used to export downloads to for external usage.", null) { + if(it != null) + context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION.or(Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION))); + if(it != null && isValidStorageUri(context, it)) { + Logger.i(TAG, "Changed external download directory: ${it}"); + Settings.instance.storage.storage_general = it.toString(); + Settings.instance.save(); + + onChanged?.invoke(getExternalDownloadDirectory(context)); + } + }; + } + fun isValidStorageUri(context: Context, uri: Uri?): Boolean { + if(uri == null) + return false; + + return context.contentResolver.persistedUriPermissions.any { it.uri == uri && it.isReadPermission && it.isWritePermission }; } //Scope @@ -171,20 +221,20 @@ class StateApp { return state; } - fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, path: Uri?, handle: (Uri?)->Unit) + fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit) { if(activity is Context) { - UIDialogs.showDialog(activity, R.drawable.ic_security, "Missing Access", "Please grant access to ${name}", null, 0, + UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Ok", { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); if(path != null) intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path); intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION - .and(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .and(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - .and(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + .or(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + .or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); activity.launchForResult(intent, 99) { if(it.resultCode == Activity.RESULT_OK) { @@ -377,16 +427,32 @@ class StateApp { val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes(); scheduleBackgroundWork(context, interval != 0, interval); - if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) { StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", { - UIDialogs.showAutomaticBackupDialog(context); - StateAnnouncement.instance.deleteAnnouncement("backup"); + if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) { + UIDialogs.toast("Missing general directory"); + changeExternalGeneralDirectory(context) { + UIDialogs.showAutomaticBackupDialog(context); + StateAnnouncement.instance.deleteAnnouncement("backup"); + }; + } + else { + UIDialogs.showAutomaticBackupDialog(context); + StateAnnouncement.instance.deleteAnnouncement("backup"); + } }, "No Backup", { Settings.instance.backup.didAskAutoBackup = true; Settings.instance.save(); }); } + else if(Settings.instance.backup.didAskAutoBackup && Settings.instance.backup.shouldAutomaticBackup() && !Settings.instance.storage.isStorageMainValid(context)) { + if(context is IWithResultLauncher) { + Logger.i(TAG, "Backup set without general directory, please select general external directory"); + changeExternalGeneralDirectory(context) { + Logger.i(TAG, "Directory set, Auto-backup should resume to this location"); + }; + } + } instance.scopeOrNull?.launch(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt index df90dcf6..c71fe8e9 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt @@ -17,11 +17,17 @@ import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo +import com.futo.platformplayer.copyTo +import com.futo.platformplayer.copyToOutputStream import com.futo.platformplayer.encryption.EncryptionProvider +import com.futo.platformplayer.getInputStream import com.futo.platformplayer.getNowDiffHours +import com.futo.platformplayer.getOutputStream import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.readBytes import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.v2.ManagedStore +import com.futo.platformplayer.writeBytes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -47,20 +53,22 @@ class StateBackup { private val _autoBackupLock = Object(); - private fun getAutomaticBackupDocumentFiles(context: Context, root: Uri, create: Boolean = false): Pair { - val dir = DocumentFile.fromTreeUri(context, root); - if(dir == null) - throw IllegalStateException("Can't access external document files"); + private fun getAutomaticBackupDocumentFiles(context: Context, create: Boolean = false): Pair { + if(!Settings.instance.storage.isStorageMainValid(context)) + return Pair(null, null); + val uri = Settings.instance.storage.getStorageGeneralUri() ?: return Pair(null, null); + val dir = DocumentFile.fromTreeUri(context, uri) ?: return Pair(null, null); val mainBackupFile = dir.findFile("GrayjayBackup.ezip") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip") else null; val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null; return Pair(mainBackupFile, secondaryBackupFile); } + /* private fun getAutomaticBackupFiles(): Pair { val dir = StateApp.instance.getExternalRootDirectory(); if(dir == null) throw IllegalStateException("Can't access external files"); return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old")) - } + }*/ fun getAllMigrationStores(): List> = listOf( @@ -77,10 +85,11 @@ class StateBackup { return password.padStart(32, '9'); } fun hasAutomaticBackup(): Boolean { - if(StateApp.instance.getExternalRootDirectory() == null) + val context = StateApp.instance.contextOrNull ?: return false; + if(!Settings.instance.storage.isStorageMainValid(context)) return false; - val files = getAutomaticBackupFiles(); - return files.first.exists() || files.second.exists(); + val files = getAutomaticBackupDocumentFiles(context,); + return files.first?.exists() ?: false || files.second?.exists() ?: false; } fun startAutomaticBackup(force: Boolean = false) { val lastBackupHoursAgo = Settings.instance.backup.lastAutoBackupTime.getNowDiffHours(); @@ -93,20 +102,27 @@ class StateBackup { try { Logger.i(TAG, "Starting AutoBackup (Last ${lastBackupHoursAgo} ago)"); synchronized(_autoBackupLock) { + val context = StateApp.instance.contextOrNull ?: return@synchronized; val data = export(); val zip = data.asZip(); val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword()); - val backupFiles = getAutomaticBackupFiles(); - val exportFile = backupFiles.first; - if (exportFile.exists()) - exportFile.copyTo(backupFiles.second, true); + if(!Settings.instance.storage.isStorageMainValid(context)) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings"); + } + } + else { + val backupFiles = getAutomaticBackupDocumentFiles(context, true); + val exportFile = backupFiles.first; + if (exportFile?.exists() == true && backupFiles.second != null) + exportFile!!.copyTo(context, backupFiles.second!!); + exportFile!!.writeBytes(context, encryptedZip); - exportFile.writeBytes(encryptedZip); - - Settings.instance.backup.lastAutoBackupTime = OffsetDateTime.now(); //OffsetDateTime.now(); - Settings.instance.save(); + Settings.instance.backup.lastAutoBackupTime = OffsetDateTime.now(); //OffsetDateTime.now(); + Settings.instance.save(); + } } Logger.i(TAG, "Finished AutoBackup"); } @@ -119,28 +135,22 @@ class StateBackup { } } - //TODO: This contains a temporary workaround to make it semi-compatible with > Android 11. By mixing "File" and "DocumentFile" usage. - //TODO: For now this is used to at least recover and gain temporary access to docs after losing access (due to permission lost after reinstall) - //TODO: Should be replaced with a more re-usable system that leverages OPEN_DOCUMENT_TREE once, and somehow persist this content after uninstall - //TODO: DocumentFiles are not compatible with normal files and require its own system. - //TODO: Investigate persistence of DOCUMENT_TREE files after uninstall... - fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false, withStream: InputStream? = null) { + //TODO: This goes has recently changed to use DocumentFiles and DocumentTree, and might need additional checks/edgecases covered. + fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false) { if(ifExists && !hasAutomaticBackup()) { Logger.i(TAG, "No AutoBackup exists, not restoring"); return; } - //TODO: Sadly on reinstalls of app this fails on file permissions. - Logger.i(TAG, "Starting AutoBackup restore"); synchronized(_autoBackupLock) { - val backupFiles = getAutomaticBackupFiles(); + val backupFiles = getAutomaticBackupDocumentFiles(StateApp.instance.context, false); try { - if (!backupFiles.first.exists() && withStream == null) + if (backupFiles.first?.exists() != true) throw IllegalStateException("Backup file does not exist"); - val backupBytesEncrypted = if(withStream != null) withStream.readBytes() else backupFiles.first.readBytes(); + val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]"); val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password)); importZipBytes(context, scope, backupBytes); Logger.i(TAG, "Finished AutoBackup restore"); @@ -154,21 +164,21 @@ class StateBackup { else null; if(activity != null) { if(activity is IWithResultLauncher) - StateApp.instance.requestDirectoryAccess(activity, "Grayjay Backup Directory", backupFiles.first.parent?.toUri()) { + StateApp.instance.requestDirectoryAccess(activity, "Grayjay Backup Directory", "Allows restoring of a backup", backupFiles.first?.parentFile?.uri) { if(it != null) { - val customFiles = StateBackup.getAutomaticBackupDocumentFiles(activity, it); + val customFiles = StateBackup.getAutomaticBackupDocumentFiles(activity); if(customFiles.first != null && customFiles.first!!.isFile && customFiles.first!!.exists() && customFiles.first!!.canRead()) - restoreAutomaticBackup(context, scope, password, ifExists, activity.contentResolver.openInputStream(customFiles.first!!.uri)); + restoreAutomaticBackup(context, scope, password, ifExists); } }; } } catch (ex: Throwable) { Logger.e(TAG, "Failed main AutoBackup restore", ex) - if (!backupFiles.second.exists()) + if (backupFiles.second?.exists() != true) throw ex; - val backupBytesEncrypted = backupFiles.second.readBytes(); + val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]"); val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password)); importZipBytes(context, scope, backupBytes); Logger.i(TAG, "Finished AutoBackup restore"); diff --git a/app/src/main/res/layout/dialog_multi_button.xml b/app/src/main/res/layout/dialog_multi_button.xml index fa37818c..979bb22c 100644 --- a/app/src/main/res/layout/dialog_multi_button.xml +++ b/app/src/main/res/layout/dialog_multi_button.xml @@ -66,6 +66,7 @@ android:orientation="horizontal" android:gravity="end" android:layout_marginTop="28dp" - android:layout_marginBottom="28dp" /> + android:layout_marginBottom="28dp"> + \ No newline at end of file diff --git a/app/src/main/res/layout/field_group.xml b/app/src/main/res/layout/field_group.xml index 637beb90..668fcd2d 100644 --- a/app/src/main/res/layout/field_group.xml +++ b/app/src/main/res/layout/field_group.xml @@ -11,7 +11,7 @@ android:id="@+id/field_group_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="16dp" + android:textSize="20dp" android:textColor="@color/white" android:fontFamily="@font/inter_light" android:text="@string/defaults"