mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 21:10:24 +02:00
New auto-backup storage using the Storage Access Framework, minor dialog tweaks, minor settings ui tweaks
This commit is contained in:
parent
5155423a1e
commit
f3f13a71dc
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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");
|
||||||
|
@ -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>
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user