New auto-backup storage using the Storage Access Framework, minor dialog tweaks, minor settings ui tweaks

This commit is contained in:
Kelvin 2023-10-12 19:18:56 +02:00
parent 5155423a1e
commit f3f13a71dc
8 changed files with 189 additions and 54 deletions

View File

@ -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) @FormField("Auto Update", "group", "Configure the auto updater", 12)
var autoUpdate = AutoUpdate(); var autoUpdate = AutoUpdate();
@Serializable @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) @FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1)
fun configureAutomaticBackup() { 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) @FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2)
fun restoreAutomaticBackup() { fun restoreAutomaticBackup() {
@ -542,6 +571,7 @@ class Settings : FragmentedStorageFileJson() {
StatePayment.instance.clearLicenses(); StatePayment.instance.clearLicenses();
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, "Licenses cleared, might require app restart"); UIDialogs.toast(it, "Licenses cleared, might require app restart");
it.reloadSettings();
} }
} }
} }

View File

@ -15,7 +15,9 @@ import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.dialogs.* import com.futo.platformplayer.dialogs.*
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -90,11 +92,25 @@ class UIDialogs {
} }
fun showAutomaticBackupDialog(context: Context) { fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
val dialog = AutomaticBackupDialog(context); val dialogAction: ()->Unit = {
registerDialogOpened(dialog); val dialog = AutomaticBackupDialog(context);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; registerDialogOpened(dialog);
dialog.show(); 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) { fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) {
val dialog = AutomaticRestoreDialog(context, scope); val dialog = AutomaticRestoreDialog(context, scope);
@ -134,10 +150,10 @@ class UIDialogs {
val buttonView = TextView(context); val buttonView = TextView(context);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt(); 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 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 { buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
if(actions.size > 1) if(actions.size > 1)
this.marginEnd = dp28; this.marginEnd = if(actions.size > 2) dp14 else dp28;
}; };
buttonView.setTextColor(Color.WHITE); buttonView.setTextColor(Color.WHITE);
buttonView.textSize = 14f; buttonView.textSize = 14f;
@ -151,8 +167,9 @@ class UIDialogs {
ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red)) ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red))
else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary)) 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) if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT)
buttonView.setPadding(dp28, dp10, dp28, dp10); buttonView.setPadding(paddingSpecialButtons, dp10, paddingSpecialButtons, dp10);
else else
buttonView.setPadding(dp10, dp10, dp10, dp10); buttonView.setPadding(dp10, dp10, dp10, dp10);

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.icu.util.Output
import android.os.Build import android.os.Build
import android.os.Looper import android.os.Looper
import android.os.OperationCanceledException import android.os.OperationCanceledException
@ -15,6 +16,7 @@ import android.view.WindowInsetsController
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformMultiClientPool 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 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 { fun loadBitmap(url: String): Bitmap {
try { try {

View File

@ -11,6 +11,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -58,13 +59,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
} }
clearFocus(); clearFocus();
dismiss(); dismiss();
Logger.i(TAG, "Set AutoBackupPassword"); Logger.i(TAG, "Set AutoBackupPassword");
Settings.instance.backup.autoBackupPassword = _editPassword.text.toString(); Settings.instance.backup.autoBackupPassword = _editPassword.text.toString();
Settings.instance.backup.didAskAutoBackup = true; Settings.instance.backup.didAskAutoBackup = true;
Settings.instance.save(); Settings.instance.save();
UIDialogs.toast(context, "AutoBackup enabled"); UIDialogs.toast(context, "AutoBackup enabled");
try { try {
StateBackup.startAutomaticBackup(true); StateBackup.startAutomaticBackup(true);
} }

View File

@ -52,10 +52,9 @@ import java.util.concurrent.TimeUnit
class StateApp { class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active 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"); private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
fun getExternalRootDirectory(): File? { fun getExternalRootDirectory(): File? {
if(!externalRootDirectory.exists()) { if(!externalRootDirectory.exists()) {
val result = externalRootDirectory.mkdirs(); val result = externalRootDirectory.mkdirs();
@ -65,6 +64,57 @@ class StateApp {
} }
else else
return externalRootDirectory; 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 //Scope
@ -171,20 +221,20 @@ class StateApp {
return state; 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) 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("Cancel", {}),
UIDialogs.Action("Ok", { UIDialogs.Action("Ok", {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if(path != null) if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) .or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.and(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) .or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.and(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); .or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
activity.launchForResult(intent, 99) { activity.launchForResult(intent, 99) {
if(it.resultCode == Activity.RESULT_OK) { if(it.resultCode == Activity.RESULT_OK) {
@ -377,16 +427,32 @@ class StateApp {
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes(); val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
scheduleBackgroundWork(context, interval != 0, interval); scheduleBackgroundWork(context, interval != 0, interval);
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) { 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", { 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); if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
StateAnnouncement.instance.deleteAnnouncement("backup"); UIDialogs.toast("Missing general directory");
changeExternalGeneralDirectory(context) {
UIDialogs.showAutomaticBackupDialog(context);
StateAnnouncement.instance.deleteAnnouncement("backup");
};
}
else {
UIDialogs.showAutomaticBackupDialog(context);
StateAnnouncement.instance.deleteAnnouncement("backup");
}
}, "No Backup", { }, "No Backup", {
Settings.instance.backup.didAskAutoBackup = true; Settings.instance.backup.didAskAutoBackup = true;
Settings.instance.save(); 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) { instance.scopeOrNull?.launch(Dispatchers.IO) {
try { try {

View File

@ -17,11 +17,17 @@ import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo 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.encryption.EncryptionProvider
import com.futo.platformplayer.getInputStream
import com.futo.platformplayer.getNowDiffHours import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.getOutputStream
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.writeBytes
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -47,20 +53,22 @@ class StateBackup {
private val _autoBackupLock = Object(); private val _autoBackupLock = Object();
private fun getAutomaticBackupDocumentFiles(context: Context, root: Uri, create: Boolean = false): Pair<DocumentFile?, DocumentFile?> { private fun getAutomaticBackupDocumentFiles(context: Context, create: Boolean = false): Pair<DocumentFile?, DocumentFile?> {
val dir = DocumentFile.fromTreeUri(context, root); if(!Settings.instance.storage.isStorageMainValid(context))
if(dir == null) return Pair(null, null);
throw IllegalStateException("Can't access external document files"); 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 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; val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
return Pair(mainBackupFile, secondaryBackupFile); return Pair(mainBackupFile, secondaryBackupFile);
} }
/*
private fun getAutomaticBackupFiles(): Pair<File, File> { private fun getAutomaticBackupFiles(): Pair<File, File> {
val dir = StateApp.instance.getExternalRootDirectory(); val dir = StateApp.instance.getExternalRootDirectory();
if(dir == null) if(dir == null)
throw IllegalStateException("Can't access external files"); throw IllegalStateException("Can't access external files");
return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old")) return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old"))
} }*/
fun getAllMigrationStores(): List<ManagedStore<*>> = listOf( fun getAllMigrationStores(): List<ManagedStore<*>> = listOf(
@ -77,10 +85,11 @@ class StateBackup {
return password.padStart(32, '9'); return password.padStart(32, '9');
} }
fun hasAutomaticBackup(): Boolean { fun hasAutomaticBackup(): Boolean {
if(StateApp.instance.getExternalRootDirectory() == null) val context = StateApp.instance.contextOrNull ?: return false;
if(!Settings.instance.storage.isStorageMainValid(context))
return false; return false;
val files = getAutomaticBackupFiles(); val files = getAutomaticBackupDocumentFiles(context,);
return files.first.exists() || files.second.exists(); return files.first?.exists() ?: false || files.second?.exists() ?: false;
} }
fun startAutomaticBackup(force: Boolean = false) { fun startAutomaticBackup(force: Boolean = false) {
val lastBackupHoursAgo = Settings.instance.backup.lastAutoBackupTime.getNowDiffHours(); val lastBackupHoursAgo = Settings.instance.backup.lastAutoBackupTime.getNowDiffHours();
@ -93,20 +102,27 @@ class StateBackup {
try { try {
Logger.i(TAG, "Starting AutoBackup (Last ${lastBackupHoursAgo} ago)"); Logger.i(TAG, "Starting AutoBackup (Last ${lastBackupHoursAgo} ago)");
synchronized(_autoBackupLock) { synchronized(_autoBackupLock) {
val context = StateApp.instance.contextOrNull ?: return@synchronized;
val data = export(); val data = export();
val zip = data.asZip(); val zip = data.asZip();
val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword()); val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
val backupFiles = getAutomaticBackupFiles(); if(!Settings.instance.storage.isStorageMainValid(context)) {
val exportFile = backupFiles.first; StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
if (exportFile.exists()) UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
exportFile.copyTo(backupFiles.second, true); }
}
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"); 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: This goes has recently changed to use DocumentFiles and DocumentTree, and might need additional checks/edgecases covered.
//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) fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false) {
//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) {
if(ifExists && !hasAutomaticBackup()) { if(ifExists && !hasAutomaticBackup()) {
Logger.i(TAG, "No AutoBackup exists, not restoring"); Logger.i(TAG, "No AutoBackup exists, not restoring");
return; return;
} }
//TODO: Sadly on reinstalls of app this fails on file permissions.
Logger.i(TAG, "Starting AutoBackup restore"); Logger.i(TAG, "Starting AutoBackup restore");
synchronized(_autoBackupLock) { synchronized(_autoBackupLock) {
val backupFiles = getAutomaticBackupFiles(); val backupFiles = getAutomaticBackupDocumentFiles(StateApp.instance.context, false);
try { try {
if (!backupFiles.first.exists() && withStream == null) if (backupFiles.first?.exists() != true)
throw IllegalStateException("Backup file does not exist"); 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)); val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
importZipBytes(context, scope, backupBytes); importZipBytes(context, scope, backupBytes);
Logger.i(TAG, "Finished AutoBackup restore"); Logger.i(TAG, "Finished AutoBackup restore");
@ -154,21 +164,21 @@ class StateBackup {
else null; else null;
if(activity != null) { if(activity != null) {
if(activity is IWithResultLauncher) 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) { 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()) 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) { catch (ex: Throwable) {
Logger.e(TAG, "Failed main AutoBackup restore", ex) Logger.e(TAG, "Failed main AutoBackup restore", ex)
if (!backupFiles.second.exists()) if (backupFiles.second?.exists() != true)
throw ex; 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)); val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
importZipBytes(context, scope, backupBytes); importZipBytes(context, scope, backupBytes);
Logger.i(TAG, "Finished AutoBackup restore"); Logger.i(TAG, "Finished AutoBackup restore");

View File

@ -66,6 +66,7 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="end" android:gravity="end"
android:layout_marginTop="28dp" android:layout_marginTop="28dp"
android:layout_marginBottom="28dp" /> android:layout_marginBottom="28dp">
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -11,7 +11,7 @@
android:id="@+id/field_group_title" android:id="@+id/field_group_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="16dp" android:textSize="20dp"
android:textColor="@color/white" android:textColor="@color/white"
android:fontFamily="@font/inter_light" android:fontFamily="@font/inter_light"
android:text="@string/defaults" android:text="@string/defaults"