feat: add remote sync capability - update scheduling logic

This commit is contained in:
Martin Filo 2023-04-05 00:47:22 +02:00
parent d50fbe566c
commit 5ec443916b
15 changed files with 148 additions and 55 deletions

View file

@ -1,36 +1,103 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.lagradost.cloudstream3.mvvm.launchSafe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
interface BackupAPI<LOGIN_DATA> {
private companion object {
val DEBOUNCE_TIME_MS = 15.seconds
companion object {
val UPLOAD_THROTTLE = 10.seconds
val DOWNLOAD_THROTTLE = 60.seconds
fun createBackupScheduler() = Scheduler<Pair<String, Boolean>>(
UPLOAD_THROTTLE.inWholeMilliseconds
) { input ->
if (input == null) {
throw IllegalStateException()
}
AccountManager.BackupApis.forEach { it.addToQueue(input.first, input.second) }
}
fun SharedPreferences.attachListener(isSettings: Boolean = true): Pair<SharedPreferences, Scheduler<Pair<String, Boolean>>> {
val scheduler = createBackupScheduler()
registerOnSharedPreferenceChangeListener { _, key ->
scheduler.work(Pair(key, isSettings))
}
return Pair(
this,
scheduler
)
}
}
fun downloadSyncData()
fun uploadSyncData()
fun shouldUpdate(): Boolean
fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean
fun Context.mergeBackup(incomingData: String)
fun Context.createBackup(loginData: LOGIN_DATA)
var uploadJob: Job?
fun addToQueue() {
if (!shouldUpdate()) {
fun addToQueue(changedKey: String, isSettings: Boolean) {
if (!shouldUpdate(changedKey, isSettings)) {
Log.d("SYNC_API", "upload not required, data is same")
return
}
uploadJob?.cancel()
if (uploadJob != null && uploadJob!!.isActive) {
Log.d("SYNC_API", "upload is canceled, scheduling new")
uploadJob?.cancel()
}
// we should ensure job will before app is closed
uploadJob = CoroutineScope(Dispatchers.IO).launchSafe {
delay(DEBOUNCE_TIME_MS)
Log.d("SYNC_API", "upload is running now")
uploadSyncData()
}
}
}
class Scheduler<INPUT>(
private val throttleTimeMs: Long,
private val onWork: (INPUT?) -> Unit
) {
private companion object {
var SCHEDULER_ID = 1
}
private val id = SCHEDULER_ID++
private val handler = Handler(Looper.getMainLooper())
private var runnable: Runnable? = null
fun work(input: INPUT? = null) {
Log.d("SYNC_API", "[$id] wants to schedule")
throttle(input)
}
fun stop() {
runnable?.let {
handler.removeCallbacks(it)
runnable = null
}
}
private fun throttle(input: INPUT?) {
stop()
runnable = Runnable {
Log.d("SYNC_API", "[$id] schedule success")
onWork(input)
}
handler.postDelayed(runnable!!, throttleTimeMs)
}
}
}

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.module.kotlin.readValue
@ -19,7 +20,6 @@ import com.google.api.services.drive.model.File
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2API
@ -29,11 +29,9 @@ import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.getBackup
import com.lagradost.cloudstream3.utils.BackupUtils.restore
import com.lagradost.cloudstream3.utils.DataStore
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import java.io.InputStream
import java.util.*
@ -68,8 +66,6 @@ class GoogleDriveApi(index: Int) :
var tempAuthFlow: AuthorizationCodeFlow? = null
var lastBackupJson: String? = null
var continuousDownloadJob: Job? = null
/////////////////////////////////////////
/////////////////////////////////////////
// OAuth2API implementation
@ -104,7 +100,7 @@ class GoogleDriveApi(index: Int) :
)
storeValue(K.TOKEN, googleTokenResponse)
startContinuousDownload()
runDownloader()
tempAuthFlow = null
return true
@ -118,22 +114,7 @@ class GoogleDriveApi(index: Int) :
return
}
startContinuousDownload()
}
private fun startContinuousDownload() {
continuousDownloadJob?.cancel()
continuousDownloadJob = CoroutineScope(Dispatchers.IO).launchSafe {
if (uploadJob?.isActive == true) {
uploadJob!!.invokeOnCompletion {
startContinuousDownload()
}
} else {
downloadSyncData()
delay(1000 * 60)
startContinuousDownload()
}
}
runDownloader()
}
override fun loginInfo(): AuthAPI.LoginInfo? {
@ -192,13 +173,11 @@ class GoogleDriveApi(index: Int) :
removeKey(it)
}
restore(
newData,
restoreSettings = true,
restoreDataStore = true
)
}
// 🤮
@ -227,6 +206,7 @@ class GoogleDriveApi(index: Int) :
val drive = getDriveService()!!
val fileName = loginData.fileName
val syncFileId = loginData.syncFileId
val ioFile = java.io.File(AcraApplication.context?.cacheDir, fileName)
lastBackupJson = getBackup().toJson()
ioFile.writeText(lastBackupJson!!)
@ -250,7 +230,10 @@ class GoogleDriveApi(index: Int) :
loginData.syncFileId = file.id
}
storeValue(K.LOGIN_DATA, loginData)
// in case we had to create new file
if (syncFileId != loginData.syncFileId) {
storeValue(K.LOGIN_DATA, loginData)
}
}
override fun downloadSyncData() {
@ -273,11 +256,13 @@ class GoogleDriveApi(index: Int) :
try {
val inputStream: InputStream = existingFile.executeMediaAsInputStream()
val content: String = inputStream.bufferedReader().use { it.readText() }
Log.d("SYNC_API", "downloadSyncData merging")
ctx.mergeBackup(content)
return
} catch (_: Exception) {
}
} else {
Log.d("SYNC_API", "downloadSyncData file not exists")
uploadSyncData()
}
}
@ -310,12 +295,14 @@ class GoogleDriveApi(index: Int) :
override fun uploadSyncData() {
val ctx = AcraApplication.context ?: return
val loginData = getLatestLoginData() ?: return
Log.d("SYNC_API", "uploadSyncData createBackup")
ctx.createBackup(loginData)
}
override fun shouldUpdate(): Boolean {
override fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean {
val ctx = AcraApplication.context ?: return false
// would be smarter to check properties, but its called once in UPLOAD_THROTTLE seconds
val newBackup = ctx.getBackup().toJson()
return lastBackupJson != newBackup
}
@ -335,6 +322,25 @@ class GoogleDriveApi(index: Int) :
/////////////////////////////////////////
/////////////////////////////////////////
// Internal
private val continuousDownloader = BackupAPI.Scheduler<Unit>(
BackupAPI.DOWNLOAD_THROTTLE.inWholeMilliseconds
) {
if (uploadJob?.isActive == true) {
uploadJob!!.invokeOnCompletion {
Log.d("SYNC_API", "upload is running, reschedule download")
runDownloader()
}
} else {
Log.d("SYNC_API", "downloadSyncData will run")
downloadSyncData()
runDownloader()
}
}
private fun runDownloader() {
continuousDownloader.work()
}
private fun getCredentialsFromStore(): Credential? {
val LOGIN_DATA = getLatestLoginData()
val TOKEN = getValue<TokenResponse>(K.TOKEN)

View file

@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
@ -664,7 +665,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
sourceDialog.subtitles_click_settings?.setOnClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx).attachListener().first
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)

View file

@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.EasterEggMonke
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
@ -137,20 +138,26 @@ class SettingsGeneral : PreferenceFragmentCompat() {
// 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()
.attachListener().first
.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()
.attachListener().first
.edit()
.putString(getString(R.string.download_path_pref), it)
.apply()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settins_general, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
fun getCurrent(): MutableList<CustomSite> {
return getKey<Array<CustomSite>>(USER_PROVIDER_API)?.toMutableList()

View file

@ -7,6 +7,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
@ -27,7 +28,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_player, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.video_buffer_length_names)

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
@ -30,7 +31,7 @@ class SettingsProviders : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_providers, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
activity?.getApiDubstatusSettings()?.let { current ->

View file

@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
@ -28,7 +29,7 @@ class SettingsUI : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settins_ui, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
getPref(R.string.poster_ui_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.poster_ui_options)

View file

@ -14,6 +14,7 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
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
@ -126,7 +127,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
}
getPref(R.string.apk_installer_key)?.setOnPreferenceClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context).attachListener().first
val prefNames = resources.getStringArray(R.array.apk_installer_pref)
val prefValues = resources.getIntArray(R.array.apk_installer_values)

View file

@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
import com.lagradost.cloudstream3.utils.SubtitleHelper
@ -42,7 +43,7 @@ class SetupFragmentLanguage : Fragment() {
normalSafeApiCall {
with(context) {
if (this == null) return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)

View file

@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_layout.*
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
@ -33,7 +34,7 @@ class SetupFragmentLayout : Fragment() {
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
val prefNames = resources.getStringArray(R.array.app_layout)
val prefValues = resources.getIntArray(R.array.app_layout_values)

View file

@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -32,7 +33,7 @@ class SetupFragmentMedia : Fragment() {
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)

View file

@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_media.*
@ -33,7 +34,7 @@ class SetupFragmentProviderLanguage : Fragment() {
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
@ -447,6 +448,7 @@ class SubtitlesFragment : Fragment() {
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
context?.let { ctx ->
PreferenceManager.getDefaultSharedPreferences(ctx)
.attachListener().first
.edit()
.putBoolean(getString(R.string.filter_sub_lang_key), b)
.apply()

View file

@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.BackupAPI
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -25,6 +25,8 @@ object DataStore {
.configure(DeserializationFeature.USE_LONG_FOR_INTS, true)
.build()
private val backupScheduler = BackupAPI.createBackupScheduler()
private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
@ -83,8 +85,7 @@ object DataStore {
val editor: SharedPreferences.Editor = prefs.edit()
editor.remove(path)
editor.apply()
AccountManager.BackupApis.forEach { it.addToQueue() }
backupScheduler.work(Pair(path, false))
}
} catch (e: Exception) {
logError(e)
@ -104,8 +105,7 @@ object DataStore {
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
editor.putString(path, mapper.writeValueAsString(value))
editor.apply()
AccountManager.BackupApis.forEach { it.addToQueue() }
backupScheduler.work(Pair(path, false))
} catch (e: Exception) {
logError(e)
}
@ -115,6 +115,7 @@ object DataStore {
setKey(getFolderName(folder, path), value)
}
inline fun <reified T : Any> String.toKotlinObject(): T {
return mapper.readValue(this, T::class.java)
}

View file

@ -23,6 +23,7 @@ import okio.buffer
import okio.sink
import java.io.File
import android.text.TextUtils
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import java.io.BufferedReader
import java.io.IOException
@ -73,7 +74,7 @@ class InAppUpdater {
private suspend fun Activity.getAppUpdate(): Update {
return try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
if (settingsManager.getBoolean(
getString(R.string.prerelease_update_key),
resources.getBoolean(R.bool.is_prerelease)
@ -254,7 +255,7 @@ class InAppUpdater {
* @param checkAutoUpdate if the update check was launched automatically
**/
suspend fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
if (!checkAutoUpdate || settingsManager.getBoolean(
getString(R.string.auto_update_key),