From 4f5dee99dfe0efefd6bb4ca45ee5d654b82cfbc7 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 4 Mar 2022 16:39:56 +0100 Subject: [PATCH] backup to file --- .../lagradost/cloudstream3/MainActivity.kt | 3 +- .../ui/settings/SettingsFragment.kt | 196 +++++++++------- .../cloudstream3/utils/BackupUtils.kt | 219 ++++++++++++++++++ .../lagradost/cloudstream3/utils/DataStore.kt | 19 ++ .../utils/VideoDownloadManager.kt | 2 +- .../res/drawable/baseline_restore_page_24.xml | 9 + .../main/res/drawable/baseline_save_as_24.xml | 10 + app/src/main/res/values/strings.xml | 10 + app/src/main/res/xml/settings.xml | 10 + 9 files changed, 386 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt create mode 100644 app/src/main/res/drawable/baseline_restore_page_24.xml create mode 100644 app/src/main/res/drawable/baseline_save_as_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 9ec6df47..ebfcf862 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos @@ -330,7 +331,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { changeStatusBarState(isEmulatorSettings()) // val navView: BottomNavigationView = findViewById(R.id.nav_view) - + setUpBackup() CommonActivity.init(this) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index a6d9d1f2..ffa216fc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -39,6 +39,8 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment +import com.lagradost.cloudstream3.utils.BackupUtils.backup +import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.HOMEPAGE_API import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.Qualities @@ -57,7 +59,7 @@ import kotlin.concurrent.thread class SettingsFragment : PreferenceFragmentCompat() { companion object { - private fun Context.getLayoutInt() : Int { + private fun Context.getLayoutInt(): Int { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) } @@ -70,11 +72,11 @@ class SettingsFragment : PreferenceFragmentCompat() { return value == 1 || value == 2 } - fun Context.isTrueTvSettings() : Boolean { + fun Context.isTrueTvSettings(): Boolean { return getLayoutInt() == 1 } - fun Context.isEmulatorSettings() : Boolean { + fun Context.isEmulatorSettings(): Boolean { return getLayoutInt() == 2 } @@ -89,31 +91,32 @@ class SettingsFragment : PreferenceFragmentCompat() { private var beneneCount = 0 // Open file picker - private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> - // It lies, it can be null if file manager quits. - if (uri == null) return@registerForActivityResult - val context = context ?: AcraApplication.context ?: return@registerForActivityResult - // RW perms for the path - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION + private val pathPicker = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + // It lies, it can be null if file manager quits. + if (uri == null) return@registerForActivityResult + val context = context ?: AcraApplication.context ?: return@registerForActivityResult + // RW perms for the path + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, flags) + context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - println("Selected URI path: $uri - Full path: ${file.filePath}") + val file = UniFile.fromUri(context, uri) + println("Selected URI path: $uri - Full path: ${file.filePath}") - // Stores the real URI using download_path_key - // Important that the URI is stored instead of filepath due to permissions. - PreferenceManager.getDefaultSharedPreferences(context) - .edit().putString(getString(R.string.download_path_key), uri.toString()).apply() - - // From URI -> File path - // File path here is purely for cosmetic purposes in settings - (file.filePath ?: uri.toString()).let { + // Stores the real URI using download_path_key + // Important that the URI is stored instead of filepath due to permissions. PreferenceManager.getDefaultSharedPreferences(context) - .edit().putString(getString(R.string.download_path_pref), it).apply() + .edit().putString(getString(R.string.download_path_key), uri.toString()).apply() + + // From URI -> File path + // File path here is purely for cosmetic purposes in settings + (file.filePath ?: uri.toString()).let { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(getString(R.string.download_path_pref), it).apply() + } } - } // idk, if you find a way of automating this it would be great // https://www.iemoji.com/view/emoji/1794/flags/antarctica @@ -173,7 +176,8 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) { val builder = - AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom).setView(R.layout.account_managment) + AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) + .setView(R.layout.account_managment) val dialog = builder.show() dialog.findViewById(R.id.account_profile_picture)?.setImage(info.profilePicture) @@ -192,29 +196,21 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + private fun getPref(id: Int): Preference? { + return try { + findPreference(getString(id)) + } catch (e : Exception) { + logError(e) + null + } + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { hideKeyboard() setPreferencesFromResource(R.xml.settings, rootKey) - - val updatePreference = findPreference(getString(R.string.manual_check_update_key))!! - val localePreference = findPreference(getString(R.string.locale_key))!! - val benenePreference = findPreference(getString(R.string.benene_count))!! - val watchQualityPreference = findPreference(getString(R.string.quality_pref_key))!! - val dnsPreference = findPreference(getString(R.string.dns_key))!! - val legalPreference = findPreference(getString(R.string.legal_notice_key))!! - val subdubPreference = findPreference(getString(R.string.display_sub_key))!! - val providerLangPreference = findPreference(getString(R.string.provider_lang_key))!! - val downloadPathPreference = findPreference(getString(R.string.download_path_key))!! - val allLayoutPreference = findPreference(getString(R.string.app_layout_key))!! - val colorPrimaryPreference = findPreference(getString(R.string.primary_color_key))!! - val preferedMediaTypePreference = findPreference(getString(R.string.prefer_media_type_key))!! - val appThemePreference = findPreference(getString(R.string.app_theme_key))!! - val subPreference = findPreference(getString(R.string.subtitle_settings_key))!! - val videoCachePreference = findPreference(getString(R.string.video_cache_key))!! val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val chromecastSubsPreference = findPreference(getString(R.string.subtitle_settings_chromecast_key))!! - videoCachePreference.setOnPreferenceClickListener { + getPref(R.string.video_cache_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_cache_size_names) val prefValues = resources.getIntArray(R.array.video_cache_size_values) @@ -234,38 +230,37 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - subPreference.setOnPreferenceClickListener { + getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener { SubtitlesFragment.push(activity, false) return@setOnPreferenceClickListener true } - chromecastSubsPreference.setOnPreferenceClickListener { + getPref(R.string.subtitle_settings_chromecast_key)?.setOnPreferenceClickListener { ChromecastSubtitlesFragment.push(activity, false) return@setOnPreferenceClickListener true } - val syncApis = listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) + val syncApis = + listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) for (sync in syncApis) { - findPreference(getString(sync.first))?.apply { + getPref(sync.first)?.apply { isVisible = accountEnabled val api = sync.second - title = getString(R.string.login_format).format(api.name, getString(R.string.account)) - setOnPreferenceClickListener { pref -> - pref.context?.let { ctx -> - val info = api.loginInfo() - if (info != null) { - showLoginInfo(api, info) - } else { - api.authenticate() - } + title = + getString(R.string.login_format).format(api.name, getString(R.string.account)) + setOnPreferenceClickListener { _ -> + val info = api.loginInfo() + if (info != null) { + showLoginInfo(api, info) + } else { + api.authenticate() } - return@setOnPreferenceClickListener true } } } - legalPreference.setOnPreferenceClickListener { + getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener { val builder: AlertDialog.Builder = AlertDialog.Builder(it.context) builder.setTitle(R.string.legal_notice) builder.setMessage(R.string.legal_notice_text) @@ -273,7 +268,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - subdubPreference.setOnPreferenceClickListener { + getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> val dublist = DubStatus.values() val names = dublist.map { it.name } @@ -300,7 +295,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - providerLangPreference.setOnPreferenceClickListener { + getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { current -> val allLangs = HashSet() for (api in apis) { @@ -346,8 +341,9 @@ class SettingsFragment : PreferenceFragmentCompat() { // app_name_download_path = Cloudstream and does not change depending on release. // DOES NOT WORK ON SCOPED STORAGE. - val secondaryDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath + - File.separator + resources.getString(R.string.app_name_download_path) + val secondaryDir = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath + + File.separator + resources.getString(R.string.app_name_download_path) val first = listOf(defaultDir, secondaryDir) (try { val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second } @@ -361,11 +357,12 @@ class SettingsFragment : PreferenceFragmentCompat() { } ?: emptyList() } - downloadPathPreference.setOnPreferenceClickListener { + getPref(R.string.download_path_key)?.setOnPreferenceClickListener { val dirs = getDownloadDirs() val currentDir = - settingsManager.getString(getString(R.string.download_path_pref), null) ?: getDownloadDir().toString() + settingsManager.getString(getString(R.string.download_path_pref), null) + ?: getDownloadDir().toString() activity?.showBottomDialog( dirs + listOf("Custom"), @@ -377,21 +374,23 @@ class SettingsFragment : PreferenceFragmentCompat() { if (it == dirs.size) { try { pathPicker.launch(Uri.EMPTY) - } catch (e : Exception) { + } catch (e: Exception) { logError(e) } } else { // Sets both visual and actual paths. // key = used path // pref = visual path - settingsManager.edit().putString(getString(R.string.download_path_key), dirs[it]).apply() - settingsManager.edit().putString(getString(R.string.download_path_pref), dirs[it]).apply() + settingsManager.edit() + .putString(getString(R.string.download_path_key), dirs[it]).apply() + settingsManager.edit() + .putString(getString(R.string.download_path_pref), dirs[it]).apply() } } return@setOnPreferenceClickListener true } - preferedMediaTypePreference.setOnPreferenceClickListener { + getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.media_type_pref) val prefValues = resources.getIntArray(R.array.media_type_pref_values) @@ -414,7 +413,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - allLayoutPreference.setOnPreferenceClickListener { + getPref(R.string.app_layout_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.app_layout) val prefValues = resources.getIntArray(R.array.app_layout_values) @@ -428,7 +427,8 @@ class SettingsFragment : PreferenceFragmentCompat() { true, {}) { try { - settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it]) + settingsManager.edit() + .putInt(getString(R.string.app_layout_key), prefValues[it]) .apply() activity?.recreate() } catch (e: Exception) { @@ -438,7 +438,17 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - colorPrimaryPreference.setOnPreferenceClickListener { + getPref(R.string.backup_key)?.setOnPreferenceClickListener { + activity?.backup() + return@setOnPreferenceClickListener true + } + + getPref(R.string.restore_key)?.setOnPreferenceClickListener { + activity?.restorePrompt() + return@setOnPreferenceClickListener true + } + + getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names) val prefValues = resources.getStringArray(R.array.themes_overlay_names_values) @@ -452,7 +462,8 @@ class SettingsFragment : PreferenceFragmentCompat() { true, {}) { try { - settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it]) + settingsManager.edit() + .putString(getString(R.string.primary_color_key), prefValues[it]) .apply() activity?.recreate() } catch (e: Exception) { @@ -462,7 +473,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - appThemePreference.setOnPreferenceClickListener { + getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names) val prefValues = resources.getStringArray(R.array.themes_names_values) @@ -476,7 +487,8 @@ class SettingsFragment : PreferenceFragmentCompat() { true, {}) { try { - settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it]) + settingsManager.edit() + .putString(getString(R.string.app_theme_key), prefValues[it]) .apply() activity?.recreate() } catch (e: Exception) { @@ -486,7 +498,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - watchQualityPreference.setOnPreferenceClickListener { + getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.quality_pref) val prefValues = resources.getIntArray(R.array.quality_pref_values) @@ -508,7 +520,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - dnsPreference.setOnPreferenceClickListener { + getPref(R.string.dns_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.dns_pref) val prefValues = resources.getIntArray(R.array.dns_pref_values) @@ -529,28 +541,32 @@ class SettingsFragment : PreferenceFragmentCompat() { try { beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) + getPref(R.string.benene_count)?.let { pref -> + pref.summary = + if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString( + R.string.benene_count_text + ).format( + beneneCount + ) - benenePreference.summary = - if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(R.string.benene_count_text).format( - beneneCount - ) + pref.setOnPreferenceClickListener { + try { + beneneCount++ + settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount) + .apply() + it.summary = getString(R.string.benene_count_text).format(beneneCount) + } catch (e: Exception) { + logError(e) + } - benenePreference.setOnPreferenceClickListener { - try { - beneneCount++ - settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount).apply() - it.summary = getString(R.string.benene_count_text).format(beneneCount) - } catch (e: Exception) { - logError(e) + return@setOnPreferenceClickListener true } - - return@setOnPreferenceClickListener true } } catch (e: Exception) { e.printStackTrace() } - updatePreference.setOnPreferenceClickListener { + getPref(R.string.manual_check_update_key)?.setOnPreferenceClickListener { thread { if (!requireActivity().runAutoUpdate(false)) { activity?.runOnUiThread { @@ -561,7 +577,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - localePreference.setOnPreferenceClickListener { pref -> + getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> val tempLangs = languages.toMutableList() if (beneneCount > 100) { tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt new file mode 100644 index 00000000..17bba8ca --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -0,0 +1,219 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.FragmentActivity +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.mapper +import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw +import com.lagradost.cloudstream3.utils.UIHelper.checkWrite +import com.lagradost.cloudstream3.utils.UIHelper.requestRW +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath +import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir +import java.io.IOException +import java.io.PrintWriter +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* + +object BackupUtils { + var restoreFileSelector: ActivityResultLauncher? = null + + // Kinda hack, but I couldn't think of a better way + data class BackupVars( + @JsonProperty("_Bool") val _Bool: Map?, + @JsonProperty("_Int") val _Int: Map?, + @JsonProperty("_String") val _String: Map?, + @JsonProperty("_Float") val _Float: Map?, + @JsonProperty("_Long") val _Long: Map?, + @JsonProperty("_StringSet") val _StringSet: Map?>?, + ) + + data class BackupFile( + @JsonProperty("datastore") val datastore: BackupVars, + @JsonProperty("settings") val settings: BackupVars + ) + + fun FragmentActivity.backup() { + try { + if (checkWrite()) { + val subDir = getBasePath().first + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) + val ext = "json" + val displayName = "CS3_Backup_${date}" + + val allData = getSharedPrefs().all + val allSettings = getDefaultSharedPrefs().all + + val allDataSorted = BackupVars( + allData.filter { it.value is Boolean } as? Map, + allData.filter { it.value is Int } as? Map, + allData.filter { it.value is String } as? Map, + allData.filter { it.value is Float } as? Map, + allData.filter { it.value is Long } as? Map, + allData.filter { it.value as? Set != null } as? Map> + ) + + val allSettingsSorted = BackupVars( + allSettings.filter { it.value is Boolean } as? Map, + allSettings.filter { it.value is Int } as? Map, + allSettings.filter { it.value is String } as? Map, + allSettings.filter { it.value is Float } as? Map, + allSettings.filter { it.value is Long } as? Map, + allSettings.filter { it.value as? Set != null } as? Map> + ) + + val backupFile = BackupFile( + allDataSorted, + allSettingsSorted + ) + val steam = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true) { + val cr = this.contentResolver + val contentUri = + MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI + //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + + val newFile = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + put(MediaStore.MediaColumns.TITLE, displayName) + put(MediaStore.MediaColumns.MIME_TYPE, "application/json") + //put(MediaStore.MediaColumns.RELATIVE_PATH, folder) + } + + val newFileUri = cr.insert( + contentUri, + newFile + ) ?: throw IOException("Error creating file uri") + cr.openOutputStream(newFileUri, "w") + ?: throw IOException("Error opening stream") + } else { + val fileName = "$displayName.$ext" + val rFile = subDir?.findFile(fileName) + if (rFile?.exists() == true) { + rFile.delete() + } + val file = + subDir?.createFile(fileName) + ?: throw IOException("Error creating file") + if (!file.exists()) throw IOException("File does not exist") + file.openOutputStream() + } + + val printStream = PrintWriter(steam) + printStream.print(mapper.writeValueAsString(backupFile)) + printStream.close() + + showToast( + this, + R.string.backup_success, + Toast.LENGTH_LONG + ) + } else { + showToast(this, getString(R.string.backup_failed), Toast.LENGTH_LONG) + requestRW() + return + } + } catch (e: Exception) { + logError(e) + try { + showToast( + this, + getString(R.string.backup_failed_error_format).format(e.toString()), + Toast.LENGTH_LONG + ) + } catch (e: Exception) { + logError(e) + } + } + } + + fun FragmentActivity.setUpBackup() { + try { + restoreFileSelector = + registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + this.let { activity -> + uri?.let { + try { + val input = + activity.contentResolver.openInputStream(uri) + ?: return@registerForActivityResult + + val restoredValue = + mapper.readValue(input) + activity.restore( + restoredValue, + restoreSettings = true, + restoreDataStore = true + ) + activity.recreate() + } catch (e: Exception) { + logError(e) + try { // smth can fail in .format + showToast( + activity, + getString(R.string.restore_failed_format).format(e.toString()) + ) + } catch (e: Exception) { + logError(e) + } + } + } + } + } + } catch (e: Exception) { + logError(e) + } + } + + fun FragmentActivity.restorePrompt() { + runOnUiThread { + restoreFileSelector?.launch("application/json") + } + } + + private fun Context.restoreMap( + map: Map?, + isEditingAppSettings: Boolean = false + ) { + map?.forEach { + setKeyRaw(it.key, it.value, isEditingAppSettings) + } + } + + fun Context.restore( + backupFile: BackupFile, + restoreSettings: Boolean, + restoreDataStore: Boolean + ) { + if (restoreSettings) { + restoreMap(backupFile.settings._Bool, true) + restoreMap(backupFile.settings._Int, true) + restoreMap(backupFile.settings._String, true) + restoreMap(backupFile.settings._Float, true) + restoreMap(backupFile.settings._Long, true) + restoreMap(backupFile.settings._StringSet, true) + } + + if (restoreDataStore) { + restoreMap(backupFile.datastore._Bool) + restoreMap(backupFile.datastore._Int) + restoreMap(backupFile.datastore._String) + restoreMap(backupFile.datastore._Float) + restoreMap(backupFile.datastore._Long) + restoreMap(backupFile.datastore._StringSet) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index e4195b8b..cd7b2248 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.SharedPreferences +import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule @@ -32,6 +33,24 @@ object DataStore { return "${folder}/${path}" } + fun Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) { + val editor: SharedPreferences.Editor = + if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit() + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + (value as? Set != null) -> editor.putStringSet(path, value as Set) + } + editor.apply() + } + + fun Context.getDefaultSharedPrefs(): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(this) + } + fun Context.getKeys(folder: String): List { return this.getSharedPrefs().all.keys.filter { it.startsWith(folder) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a11cd7f3..92ff7201 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1043,7 +1043,7 @@ object VideoDownloadManager { return basePathToFile(this, basePathSetting) to basePathSetting } - private fun UniFile?.isDownloadDir(): Boolean { + fun UniFile?.isDownloadDir(): Boolean { return this != null && this.filePath == getDownloadDir()?.filePath } diff --git a/app/src/main/res/drawable/baseline_restore_page_24.xml b/app/src/main/res/drawable/baseline_restore_page_24.xml new file mode 100644 index 00000000..f899b96e --- /dev/null +++ b/app/src/main/res/drawable/baseline_restore_page_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_save_as_24.xml b/app/src/main/res/drawable/baseline_save_as_24.xml new file mode 100644 index 00000000..5427cddf --- /dev/null +++ b/app/src/main/res/drawable/baseline_save_as_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ba76b63..c8546a9a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,8 @@ Cloudstream app_layout_key primary_color_key + restore_key + backup_key prefer_media_type_key app_theme_key @@ -195,6 +197,14 @@ overlay + Restore data from backup + Backup data + Loaded backup file + Failed to restore data from file %s + Successfully stored data + Storage permissions missing, please try again + Error backing up %s + Search Info Advanced Search diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 8eadf45d..29e53091 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -169,6 +169,16 @@ app:key="@string/manual_check_update_key" app:icon="@drawable/ic_baseline_system_update_24" /> + + + +