Automatic backups (#592)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
This commit is contained in:
self-similarity 2023-10-10 15:19:27 +00:00 committed by GitHub
parent abbad1bc94
commit 91b195241e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 35 deletions

View file

@ -1110,7 +1110,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (appVer != lastAppAutoBackup) { if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME) setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
normalSafeApiCall { normalSafeApiCall {
backup() backup(this)
} }
normalSafeApiCall { normalSafeApiCall {
// Recompile oat on new version // Recompile oat on new version

View file

@ -0,0 +1,96 @@
package com.lagradost.cloudstream3.services
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import java.util.concurrent.TimeUnit
const val BACKUP_CHANNEL_ID = "cloudstream3.backups"
const val BACKUP_WORK_NAME = "work_backup"
const val BACKUP_CHANNEL_NAME = "Backups"
const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups"
const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique
class BackupWorkManager(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
companion object {
fun enqueuePeriodicWork(context: Context?, intervalHours: Long) {
if (context == null) return
if (intervalHours == 0L) {
WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME)
return
}
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.build()
val periodicSyncDataWork =
PeriodicWorkRequest.Builder(
BackupWorkManager::class.java,
intervalHours,
TimeUnit.HOURS
)
.addTag(BACKUP_WORK_NAME)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
BACKUP_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
periodicSyncDataWork
)
// Uncomment below for testing
// val oneTimeBackupWork =
// OneTimeWorkRequest.Builder(BackupWorkManager::class.java)
// .addTag(BACKUP_WORK_NAME)
// .setConstraints(constraints)
// .build()
//
// WorkManager.getInstance(context).enqueue(oneTimeBackupWork)
}
}
private val backupNotificationBuilder =
NotificationCompat.Builder(context, BACKUP_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setAutoCancel(true)
.setContentTitle(context.getString(R.string.pref_category_backup))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
override suspend fun doWork(): Result {
context.createNotificationChannel(
BACKUP_CHANNEL_ID,
BACKUP_CHANNEL_NAME,
BACKUP_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
BACKUP_NOTIFICATION_ID,
backupNotificationBuilder.build()
)
)
BackupUtils.backup(context)
return Result.success()
}
}

View file

@ -19,14 +19,16 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.services.BackupWorkManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.backup_key)?.setOnPreferenceClickListener { getPref(R.string.backup_key)?.setOnPreferenceClickListener {
activity?.backup() BackupUtils.backup(activity)
return@setOnPreferenceClickListener true
}
getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val prefNames = resources.getStringArray(R.array.periodic_work_names)
val prefValues = resources.getIntArray(R.array.periodic_work_values)
val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0)
activity?.showDialog(
prefNames.toList(),
prefValues.indexOf(current),
getString(R.string.backup_frequency),
true,
{}) { index ->
settingsManager.edit()
.putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply()
BackupWorkManager.enqueuePeriodicWork(
context ?: AcraApplication.context,
prefValues[index].toLong()
)
}
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
@ -65,7 +90,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val builder = val builder =
AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) AlertDialog.Builder(pref.context, R.style.AlertDialogCustom)
val binding = LogcatBinding.inflate(layoutInflater,null,false ) val binding = LogcatBinding.inflate(layoutInflater, null, false)
builder.setView(binding.root) builder.setView(binding.root)
val dialog = builder.create() val dialog = builder.create()
@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
val prefNames = resources.getStringArray(R.array.auto_download_plugin) val prefNames = resources.getStringArray(R.array.auto_download_plugin)
val prefValues = enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value } val prefValues =
enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value }
val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0)
@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
getString(R.string.automatic_plugin_download_mode_title), getString(R.string.automatic_plugin_download_mode_title),
true, true,
{}) { {}) {
settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() settingsManager.edit()
.putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
} }
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true

View file

@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -90,9 +91,11 @@ object BackupUtils {
) )
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun Context.getBackup(): BackupFile { private fun getBackup(context: Context?): BackupFile? {
val allData = getSharedPrefs().all.filter { it.key.isTransferable() } if (context == null) return null
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
val allDataSorted = BackupVars( val allDataSorted = BackupVars(
allData.filter { it.value is Boolean } as? Map<String, Boolean>, allData.filter { it.value is Boolean } as? Map<String, Boolean>,
@ -119,46 +122,50 @@ object BackupUtils {
} }
@WorkerThread @WorkerThread
fun Context.restore( fun restore(
context: Context?,
backupFile: BackupFile, backupFile: BackupFile,
restoreSettings: Boolean, restoreSettings: Boolean,
restoreDataStore: Boolean restoreDataStore: Boolean
) { ) {
if (context == null) return
if (restoreSettings) { if (restoreSettings) {
restoreMap(backupFile.settings._Bool, true) context.restoreMap(backupFile.settings._Bool, true)
restoreMap(backupFile.settings._Int, true) context.restoreMap(backupFile.settings._Int, true)
restoreMap(backupFile.settings._String, true) context.restoreMap(backupFile.settings._String, true)
restoreMap(backupFile.settings._Float, true) context.restoreMap(backupFile.settings._Float, true)
restoreMap(backupFile.settings._Long, true) context.restoreMap(backupFile.settings._Long, true)
restoreMap(backupFile.settings._StringSet, true) context.restoreMap(backupFile.settings._StringSet, true)
} }
if (restoreDataStore) { if (restoreDataStore) {
restoreMap(backupFile.datastore._Bool) context.restoreMap(backupFile.datastore._Bool)
restoreMap(backupFile.datastore._Int) context.restoreMap(backupFile.datastore._Int)
restoreMap(backupFile.datastore._String) context.restoreMap(backupFile.datastore._String)
restoreMap(backupFile.datastore._Float) context.restoreMap(backupFile.datastore._Float)
restoreMap(backupFile.datastore._Long) context.restoreMap(backupFile.datastore._Long)
restoreMap(backupFile.datastore._StringSet) context.restoreMap(backupFile.datastore._StringSet)
} }
} }
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
fun FragmentActivity.backup() = ioSafe { fun backup(context: Context?) = ioSafe {
if (context == null) return@ioSafe
var fileStream: OutputStream? = null var fileStream: OutputStream? = null
var printStream: PrintWriter? = null var printStream: PrintWriter? = null
try { try {
if (!checkWrite()) { if (!context.checkWrite()) {
showToast(R.string.backup_failed, Toast.LENGTH_LONG) showToast(R.string.backup_failed, Toast.LENGTH_LONG)
requestRW() context.getActivity()?.requestRW()
return@ioSafe return@ioSafe
} }
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
val ext = "txt" val ext = "txt"
val displayName = "CS3_Backup_${date}" val displayName = "CS3_Backup_${date}"
val backupFile = getBackup() val backupFile = getBackup(context)
val stream = setupStream(this@backup, displayName, null, ext, false) val stream = setupStream(context, displayName, null, ext, false)
fileStream = stream.openNew() fileStream = stream.openNew()
printStream = PrintWriter(fileStream) printStream = PrintWriter(fileStream)
@ -198,7 +205,8 @@ object BackupUtils {
val restoredValue = val restoredValue =
mapper.readValue<BackupFile>(input) mapper.readValue<BackupFile>(input)
activity.restore( restore(
activity,
restoredValue, restoredValue,
restoreSettings = true, restoreSettings = true,
restoreDataStore = true restoreDataStore = true

View file

@ -71,7 +71,7 @@ object UIHelper {
val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt()
val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density)
fun Activity.checkWrite(): Boolean { fun Context.checkWrite(): Boolean {
return (ContextCompat.checkSelfPermission( return (ContextCompat.checkSelfPermission(
this, this,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE

View file

@ -41,7 +41,7 @@
<item>0</item> <item>0</item>
<item>1</item> <item>1</item>
</array> </array>
<!-- MainAPI enum: AutoDownloadMode --> <!-- MainAPI enum: AutoDownloadMode -->
<array name="auto_download_plugin"> <array name="auto_download_plugin">
<item>@string/disable</item> <item>@string/disable</item>
@ -126,6 +126,26 @@
<item>30min</item> <item>30min</item>
</array> </array>
<string-array name="periodic_work_names">
<item>@string/none</item>
<item>3h</item>
<item>6h</item>
<item>12h</item>
<item>24h</item>
<item>3d</item>
<item>7d</item>
</string-array>
<!-- Values in hours -->
<integer-array name="periodic_work_values">
<item>0</item>
<item>3</item>
<item>6</item>
<item>12</item>
<item>24</item>
<item>72</item>
<item>168</item>
</integer-array>
<array name="video_buffer_length_values"> <array name="video_buffer_length_values">
<item>0</item> <item>0</item>
<item>60</item> <item>60</item>

View file

@ -51,6 +51,7 @@
<string name="primary_color_key" translatable="false">primary_color_key</string> <string name="primary_color_key" translatable="false">primary_color_key</string>
<string name="restore_key" translatable="false">restore_key</string> <string name="restore_key" translatable="false">restore_key</string>
<string name="backup_key" translatable="false">backup_key</string> <string name="backup_key" translatable="false">backup_key</string>
<string name="automatic_backup_key" translatable="false">automatic_backup_key</string>
<string name="prefer_media_type_key" translatable="false">prefer_media_type_key_2</string> <string name="prefer_media_type_key" translatable="false">prefer_media_type_key_2</string>
<string name="app_theme_key" translatable="false">app_theme_key</string> <string name="app_theme_key" translatable="false">app_theme_key</string>
<string name="episode_sync_enabled_key" translatable="false">episode_sync_enabled_key</string> <string name="episode_sync_enabled_key" translatable="false">episode_sync_enabled_key</string>
@ -229,6 +230,7 @@
<string name="episode_sync_settings_des">Automatically sync your current episode progress</string> <string name="episode_sync_settings_des">Automatically sync your current episode progress</string>
<string name="restore_settings">Restore data from backup</string> <string name="restore_settings">Restore data from backup</string>
<string name="backup_settings">Back up data</string> <string name="backup_settings">Back up data</string>
<string name="backup_frequency">Backup frequency</string>
<string name="restore_success">Loaded backup file</string> <string name="restore_success">Loaded backup file</string>
<string name="restore_failed_format" formatted="true">Failed to restore data from file %s</string> <string name="restore_failed_format" formatted="true">Failed to restore data from file %s</string>
<string name="backup_success">Data stored</string> <string name="backup_success">Data stored</string>

View file

@ -9,7 +9,7 @@
android:summaryOn="@string/bug_report_settings_on" android:summaryOn="@string/bug_report_settings_on"
android:title="@string/pref_disable_acra" /> android:title="@string/pref_disable_acra" />
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_category_app_updates"> android:title="@string/pref_category_app_updates">
<Preference <Preference
android:title="@string/check_for_update" android:title="@string/check_for_update"
@ -37,27 +37,32 @@
app:key="@string/auto_update_key" /> app:key="@string/auto_update_key" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_category_backup"> android:title="@string/pref_category_backup">
<Preference <Preference
android:icon="@drawable/baseline_save_as_24" android:icon="@drawable/baseline_save_as_24"
android:key="@string/backup_key" android:key="@string/backup_key"
android:title="@string/backup_settings" /> android:title="@string/backup_settings" />
<Preference
android:icon="@drawable/baseline_save_as_24"
android:key="@string/automatic_backup_key"
android:title="@string/backup_frequency" />
<Preference <Preference
android:icon="@drawable/baseline_restore_page_24" android:icon="@drawable/baseline_restore_page_24"
android:key="@string/restore_key" android:key="@string/restore_key"
android:title="@string/restore_settings" /> android:title="@string/restore_settings" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_category_extensions"> android:title="@string/pref_category_extensions">
<SwitchPreference <SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:icon="@drawable/ic_baseline_extension_24" android:icon="@drawable/ic_baseline_extension_24"
android:key="@string/auto_update_plugins_key" android:key="@string/auto_update_plugins_key"
android:title="@string/automatic_plugin_updates" /> android:title="@string/automatic_plugin_updates" />
<Preference <Preference
android:icon="@drawable/ic_baseline_extension_24" android:icon="@drawable/ic_baseline_extension_24"
android:key="@string/auto_download_plugins_key" android:key="@string/auto_download_plugins_key"
@ -65,7 +70,7 @@
android:summary="@string/automatic_plugin_download_summary" /> android:summary="@string/automatic_plugin_download_summary" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_category_actions"> android:title="@string/pref_category_actions">
<Preference <Preference
android:icon="@drawable/baseline_description_24" android:icon="@drawable/baseline_description_24"