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) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
normalSafeApiCall {
backup()
backup(this)
}
normalSafeApiCall {
// 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.mvvm.logError
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.setPaddingBottom
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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
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.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
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
}
@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
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)
@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
getString(R.string.automatic_plugin_download_mode_title),
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) }
}
return@setOnPreferenceClickListener true

View file

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

View file

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

View file

@ -126,6 +126,26 @@
<item>30min</item>
</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">
<item>0</item>
<item>60</item>

View file

@ -51,6 +51,7 @@
<string name="primary_color_key" translatable="false">primary_color_key</string>
<string name="restore_key" translatable="false">restore_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="app_theme_key" translatable="false">app_theme_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="restore_settings">Restore data from backup</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_failed_format" formatted="true">Failed to restore data from file %s</string>
<string name="backup_success">Data stored</string>

View file

@ -44,6 +44,11 @@
android:key="@string/backup_key"
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
android:icon="@drawable/baseline_restore_page_24"
android:key="@string/restore_key"