feat: add remote sync capability - refactor, refresh ui on restore and improve data restore (pt.2)

This commit is contained in:
Martin Filo 2023-05-01 18:01:26 +02:00
parent e1e039b58c
commit fe36b69758
21 changed files with 363 additions and 227 deletions

View file

@ -264,6 +264,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>()
val afterBackupRestoreEvent = Event<Unit>()
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>()

View file

@ -2,8 +2,6 @@ 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.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.mvvm.launchSafe
@ -11,6 +9,7 @@ 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.Scheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -27,6 +26,7 @@ interface BackupAPI<LOGIN_DATA> {
)
data class PreferencesSchedulerData(
val prefs: SharedPreferences,
val storeKey: String,
val isSettings: Boolean
)
@ -46,41 +46,9 @@ interface BackupAPI<LOGIN_DATA> {
// on factors like: live devices, quota limits, etc
val UPLOAD_THROTTLE = 10.seconds
val DOWNLOAD_THROTTLE = 60.seconds
// add to queue may be called frequently
private val ioScope = CoroutineScope(Dispatchers.IO)
fun createBackupScheduler() = Scheduler<PreferencesSchedulerData>(
UPLOAD_THROTTLE.inWholeMilliseconds
) { input ->
if (input == null) {
throw IllegalStateException()
}
AccountManager.BackupApis.forEach { it.addToQueue(input.storeKey, input.isSettings) }
}
// Common usage is `val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().self`
// which means it is mostly used for settings preferences, therefore we use `isSettings: Boolean = true`, be careful
// to turn it of if you need to directly access `context.getSharedPreferences` (without using DataStore)
fun SharedPreferences.attachBackupListener(
isSettings: Boolean = true,
syncPrefs: SharedPreferences? = null
): SharedPreferencesWithListener {
val scheduler = createBackupScheduler()
registerOnSharedPreferenceChangeListener { _, storeKey ->
syncPrefs?.logHistoryChanged(storeKey, BackupUtils.RestoreSource.SETTINGS)
scheduler.work(PreferencesSchedulerData(storeKey, isSettings))
}
return SharedPreferencesWithListener(this, scheduler)
}
fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences?): SharedPreferencesWithListener {
return attachBackupListener(true, syncPrefs)
}
fun SharedPreferences.logHistoryChanged(path: String, source: BackupUtils.RestoreSource) {
edit().putLong("$SYNC_HISTORY_PREFIX${source.prefix}$path", System.currentTimeMillis())
.apply()
@ -89,11 +57,12 @@ interface BackupAPI<LOGIN_DATA> {
/**
* Should download data from API and call Context.mergeBackup(incomingData: String). If data
* does not exist on the api uploadSyncData() is recommended to call
* does not exist on the api uploadSyncData() is recommended to call. Should be called with
* overwrite=true when user ads new account so it would accept changes from API
* @see Context.mergeBackup
* @see uploadSyncData
*/
fun downloadSyncData()
fun downloadSyncData(overwrite: Boolean)
/**
* Should upload data to API and call Context.createBackup(loginData: LOGIN_DATA)
@ -103,22 +72,24 @@ interface BackupAPI<LOGIN_DATA> {
fun Context.createBackup(loginData: LOGIN_DATA)
fun Context.mergeBackup(incomingData: String) {
val currentData = getBackup()
fun Context.mergeBackup(incomingData: String, overwrite: Boolean) {
val newData = DataStore.mapper.readValue<BackupUtils.BackupFile>(incomingData)
if (overwrite) {
Log.d(LOG_KEY, "overwriting data")
restore(newData)
val keysToUpdate = getKeysToUpdate(currentData, newData)
if (keysToUpdate.isEmpty()) {
return
}
restore(
newData,
keysToUpdate,
restoreSettings = true,
restoreDataStore = true,
restoreSyncData = true
)
val keysToUpdate = getKeysToUpdate(getBackup(), newData)
if (keysToUpdate.isEmpty()) {
Log.d(LOG_KEY, "remote data is up to date, sync not needed")
return
}
Log.d(LOG_KEY, incomingData)
restore(newData, keysToUpdate)
}
var uploadJob: Job?
@ -153,7 +124,7 @@ interface BackupAPI<LOGIN_DATA> {
}
val failed = result?.failed() ?: true
Log.d(LOG_KEY, "JSON comparison took $executionTime ms, compareFailed=$failed")
Log.d(LOG_KEY, "JSON comparison took $executionTime ms, compareFailed=$failed, result=$result")
return JSONComparison(failed, result)
}
@ -161,34 +132,25 @@ interface BackupAPI<LOGIN_DATA> {
fun getKeysToUpdate(
currentData: BackupUtils.BackupFile,
newData: BackupUtils.BackupFile
): List<String> {
val currentSync = currentData.syncMeta._Long.orEmpty().filter {
it.key.startsWith(SYNC_HISTORY_PREFIX)
}
val newSync = newData.syncMeta._Long.orEmpty().filter {
it.key.startsWith(SYNC_HISTORY_PREFIX)
}
): Set<String> {
val currentSync = getSyncKeys(currentData)
val newSync = getSyncKeys(newData)
val changedKeys = newSync.filter {
val localTimestamp = if (currentSync[it.key] != null) {
currentSync[it.key]!!
} else {
0L
}
val localTimestamp = currentSync[it.key] ?: 0L
it.value > localTimestamp
}.keys
val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) }
val missingKeys = getMissingKeys(currentData, newData) - changedKeys
return mutableListOf(
*missingKeys.toTypedArray(),
*onlyLocalKeys.toTypedArray(),
*changedKeys.toTypedArray()
)
val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) }
val missingKeys = getMissingKeys(currentData, newData)
return (missingKeys + onlyLocalKeys + changedKeys).toSet()
}
private fun getSyncKeys(data: BackupUtils.BackupFile) =
data.syncMeta._Long.orEmpty().filter { it.key.startsWith(SYNC_HISTORY_PREFIX) }
// 🤮
private fun getMissingKeys(
old: BackupUtils.BackupFile,
@ -211,44 +173,4 @@ interface BackupAPI<LOGIN_DATA> {
private fun getMissing(old: Map<String, *>?, new: Map<String, *>?): Array<String> =
new.orEmpty().keys.subtract(old.orEmpty().keys).toTypedArray()
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(LOG_KEY, "[$id] wants to schedule [${input}]")
throttle(input)
}
fun workNow(input: INPUT? = null) {
Log.d(LOG_KEY, "[$id] runs immediate [${input}]")
stop()
onWork(input)
}
fun stop() {
runnable?.let {
handler.removeCallbacks(it)
runnable = null
}
}
private fun throttle(input: INPUT?) {
stop()
runnable = Runnable {
Log.d(LOG_KEY, "[$id] schedule success")
onWork(input)
}
handler.postDelayed(runnable!!, throttleTimeMs)
}
}
}

View file

@ -35,7 +35,9 @@ interface InAppOAuth2API : OAuth2API {
abstract class InAppOAuth2APIManager(defIndex: Int) : AccountManager(defIndex), InAppOAuth2API {
enum class K {
LOGIN_DATA,
TOKEN;
IS_READY,
TOKEN,
;
val value: String = "data_oauth2_$name"
}

View file

@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.syncproviders.InAppOAuth2APIManager
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.BackupUtils.getBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Scheduler
import kotlinx.coroutines.Job
import java.io.InputStream
import java.util.Date
@ -38,16 +39,16 @@ import java.util.Date
*
* | State | Priority | Description
* |---------:|:--------:|---------------------------------------------------------------------
* | Progress | 2 | Restoring backup should update view models
* | Progress | 1 | Check if data was really changed when calling backupscheduler.work then
* | | | dont update sync meta if not needed
* | Waiting | 2 | Add button to manually trigger sync
* | Waiting | 3 | Move "https://chiff.github.io/cloudstream-sync/google-drive"
* | Waiting | 3 | We should check what keys should really be restored. If user has multiple
* | | | devices with different settings that they want to keep we should respect that
* | Waiting | 4 | Implement backup before user quits application
* | Waiting | 5 | Choose what should be synced
* | Waiting | 5 | Choose what should be synced and recheck `invalidKeys` in createBackupScheduler
* | Someday | 3 | Add option to use proper OAuth through Google Services One Tap
* | Someday | 5 | Encrypt data on Drive (low priority)
* | Solved | 1 | Racing conditions when multiple devices in use
* | Solved | 2 | Restoring backup should update view models
*/
class GoogleDriveApi(index: Int) :
InAppOAuth2APIManager(index),
@ -107,8 +108,9 @@ class GoogleDriveApi(index: Int) :
)
storeValue(K.TOKEN, googleTokenResponse)
runDownloader(true)
runDownloader(runNow = true, overwrite = true)
storeValue(K.IS_READY, true)
tempAuthFlow = null
return true
}
@ -147,6 +149,7 @@ class GoogleDriveApi(index: Int) :
switchToNewAccount()
}
storeValue(K.IS_READY, false)
storeValue(K.LOGIN_DATA, data)
val authFlow = GAPI.createAuthFlow(data.clientId, data.secret)
@ -211,7 +214,7 @@ class GoogleDriveApi(index: Int) :
}
}
override fun downloadSyncData() {
override fun downloadSyncData(overwrite: Boolean) {
val ctx = AcraApplication.context ?: return
val drive = getDriveService() ?: return
val loginData = getLatestLoginData() ?: return
@ -220,7 +223,8 @@ class GoogleDriveApi(index: Int) :
val existingFile = if (existingFileId != null) {
try {
drive.files().get(existingFileId)
} catch (_: Exception) {
} catch (e: Exception) {
Log.e(LOG_KEY, "Could not find file for id $existingFileId", e)
null
}
} else {
@ -232,19 +236,27 @@ class GoogleDriveApi(index: Int) :
val inputStream: InputStream = existingFile.executeMediaAsInputStream()
val content: String = inputStream.bufferedReader().use { it.readText() }
Log.d(LOG_KEY, "downloadSyncData merging")
ctx.mergeBackup(content)
ctx.mergeBackup(content, overwrite)
return
} catch (e: Exception) {
Log.e(LOG_KEY,"download failed", e)
Log.e(LOG_KEY, "download failed", e)
}
} else {
Log.d(LOG_KEY, "downloadSyncData file not exists")
uploadSyncData()
}
// if failed
Log.d(LOG_KEY, "downloadSyncData file not exists")
uploadSyncData()
}
private fun getOrCreateSyncFileId(drive: Drive, loginData: InAppOAuth2API.LoginData): String? {
val existingFileId: String? = loginData.syncFileId ?: drive
if (loginData.syncFileId != null) {
val verified = drive.files().get(loginData.syncFileId)
if (verified != null) {
return loginData.syncFileId
}
}
val existingFileId: String? = drive
.files()
.list()
.setQ("name='${loginData.fileName}' and trashed=false")
@ -253,29 +265,38 @@ class GoogleDriveApi(index: Int) :
?.getOrNull(0)
?.id
if (loginData.syncFileId == null) {
if (existingFileId != null) {
loginData.syncFileId = existingFileId
storeValue(K.LOGIN_DATA, loginData)
if (existingFileId != null) {
loginData.syncFileId = existingFileId
storeValue(K.LOGIN_DATA, loginData)
return existingFileId
}
return null
return existingFileId
}
val verifyId = drive.files().get(existingFileId)
return if (verifyId == null) {
return null
} else {
existingFileId
}
return null
}
override fun uploadSyncData() {
val ctx = AcraApplication.context ?: return
val loginData = getLatestLoginData() ?: return
Log.d(LOG_KEY, "uploadSyncData createBackup")
val canUpload = getValue<Boolean>(K.IS_READY)
if (canUpload != true) {
Log.d(LOG_KEY, "uploadSyncData is not ready yet")
return
}
val ctx = AcraApplication.context
val loginData = getLatestLoginData()
if (ctx == null) {
Log.d(LOG_KEY, "uploadSyncData cannot run (ctx)")
return
}
if (loginData == null) {
Log.d(LOG_KEY, "uploadSyncData cannot run (loginData)")
return
}
Log.d(LOG_KEY, "uploadSyncData will run")
ctx.createBackup(loginData)
}
@ -301,29 +322,28 @@ class GoogleDriveApi(index: Int) :
/////////////////////////////////////////
/////////////////////////////////////////
// Internal
private val continuousDownloader = BackupAPI.Scheduler<Unit>(
BackupAPI.DOWNLOAD_THROTTLE.inWholeMilliseconds
) {
if (uploadJob?.isActive == true) {
uploadJob!!.invokeOnCompletion {
Log.d(LOG_KEY, "upload is running, reschedule download")
private val continuousDownloader = Scheduler<Boolean>(
BackupAPI.DOWNLOAD_THROTTLE.inWholeMilliseconds,
{ overwrite ->
if (uploadJob?.isActive == true) {
uploadJob!!.invokeOnCompletion {
Log.d(LOG_KEY, "upload is running, reschedule download")
runDownloader(false, overwrite == true)
}
} else {
Log.d(LOG_KEY, "downloadSyncData will run")
ioSafe {
downloadSyncData(overwrite == true)
}
runDownloader()
}
} else {
Log.d(LOG_KEY, "downloadSyncData will run")
ioSafe {
downloadSyncData()
}
runDownloader()
}
}
})
private fun runDownloader(runNow: Boolean = false) {
private fun runDownloader(runNow: Boolean = false, overwrite: Boolean = false) {
if (runNow) {
continuousDownloader.workNow()
continuousDownloader.workNow(overwrite)
} else {
continuousDownloader.work()
continuousDownloader.work(overwrite)
}
}

View file

@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent
@ -477,16 +478,18 @@ class HomeFragment : Fragment() {
bookmarksUpdatedEvent += ::bookmarksUpdated
afterPluginsLoadedEvent += ::afterPluginsLoaded
mainPluginsLoadedEvent += ::afterMainPluginsLoaded
afterBackupRestoreEvent += ::reloadStored
}
override fun onStop() {
bookmarksUpdatedEvent -= ::bookmarksUpdated
afterPluginsLoadedEvent -= ::afterPluginsLoaded
mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
afterBackupRestoreEvent -= ::reloadStored
super.onStop()
}
private fun reloadStored() {
private fun reloadStored(unused: Unit = Unit) {
homeViewModel.loadResumeWatching()
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {

View file

@ -6,6 +6,7 @@ import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
@ -21,10 +22,12 @@ import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
@ -38,6 +41,7 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.fragment_library.*
import org.checkerframework.framework.qual.Unused
import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder"
@ -76,9 +80,15 @@ class LibraryFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
MainActivity.afterBackupRestoreEvent += ::onNewSyncData
return inflater.inflate(R.layout.fragment_library, container, false)
}
override fun onDestroyView() {
super.onDestroyView()
MainActivity.afterBackupRestoreEvent -= ::onNewSyncData
}
override fun onSaveInstanceState(outState: Bundle) {
viewpager?.currentItem?.let { currentItem ->
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
@ -386,6 +396,21 @@ class LibraryFragment : Fragment() {
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig)
}
override fun onResume() {
super.onResume()
MainActivity.afterBackupRestoreEvent += ::onNewSyncData
}
override fun onStop() {
super.onStop()
MainActivity.afterBackupRestoreEvent -= ::onNewSyncData
}
private fun onNewSyncData(unused: Unit) {
Log.d(BackupAPI.LOG_KEY, "will reload pages")
libraryViewModel.reloadPages(true)
}
}
class MenuSearchView(context: Context) : SearchView(context) {

View file

@ -28,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
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

View file

@ -25,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.EasterEggMonke
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom

View file

@ -7,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
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

View file

@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom

View file

@ -8,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
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

View file

@ -14,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
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

View file

@ -15,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs

View file

@ -10,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_layout.acra_switch

View file

@ -12,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar

View file

@ -14,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar

View file

@ -27,7 +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.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.DataStore.setKey

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@ -14,6 +15,7 @@ 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.MainActivity.Companion.afterBackupRestoreEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
@ -70,6 +72,7 @@ object BackupUtils {
MAL_UNIXTIME_KEY,
MAL_USER_KEY,
InAppOAuth2APIManager.K.TOKEN.value,
InAppOAuth2APIManager.K.IS_READY.value,
// The plugins themselves are not backed up
PLUGINS_KEY,
@ -87,6 +90,16 @@ object BackupUtils {
private var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null
// Kinda hack, but I couldn't think of a better way
data class RestoreMapData(
val wantToRestore: MutableSet<String> = mutableSetOf(),
val successfulRestore: MutableSet<String> = mutableSetOf()
) {
fun addAll(data: RestoreMapData) {
wantToRestore.addAll(data.wantToRestore)
successfulRestore.addAll(data.successfulRestore)
}
}
data class BackupVars(
@JsonProperty("_Bool") val _Bool: Map<String, Boolean>?,
@JsonProperty("_Int") val _Int: Map<String, Int>?,
@ -99,8 +112,32 @@ object BackupUtils {
data class BackupFile(
@JsonProperty("datastore") val datastore: BackupVars,
@JsonProperty("settings") val settings: BackupVars,
@JsonProperty("sync-meta") val syncMeta: BackupVars
)
@JsonProperty("sync-meta") val syncMeta: BackupVars,
) {
fun restore(
ctx: Context,
source: RestoreSource,
restoreKeys: Set<String>? = null
): RestoreMapData {
val data = getData(source)
val successfulRestore = RestoreMapData()
successfulRestore.addAll(ctx.restoreMap(data._Bool, source, restoreKeys))
successfulRestore.addAll(ctx.restoreMap(data._Int, source, restoreKeys))
successfulRestore.addAll(ctx.restoreMap(data._String, source, restoreKeys))
successfulRestore.addAll(ctx.restoreMap(data._Float, source, restoreKeys))
successfulRestore.addAll(ctx.restoreMap(data._Long, source, restoreKeys))
successfulRestore.addAll(ctx.restoreMap(data._StringSet, source, restoreKeys))
return successfulRestore
}
private fun getData(source: RestoreSource) = when (source) {
RestoreSource.SYNC -> syncMeta
RestoreSource.DATA -> datastore
RestoreSource.SETTINGS -> settings
}
}
@Suppress("UNCHECKED_CAST")
fun Context.getBackup(): BackupFile {
@ -142,40 +179,38 @@ object BackupUtils {
)
}
@WorkerThread
fun Context.restore(backupFile: BackupFile, restoreKeys: Set<String>? = null) = restore(
backupFile,
restoreKeys,
RestoreSource.SYNC,
RestoreSource.DATA,
RestoreSource.SETTINGS
)
@WorkerThread
fun Context.restore(
backupFile: BackupFile,
restoreKeys: List<String>? = null,
restoreSettings: Boolean,
restoreDataStore: Boolean,
restoreSyncData: Boolean
restoreKeys: Set<String>? = null,
vararg restoreSources: RestoreSource
) {
if (restoreSyncData) {
restoreMap(backupFile.syncMeta._Bool, RestoreSource.SYNC, restoreKeys)
restoreMap(backupFile.syncMeta._Int, RestoreSource.SYNC, restoreKeys)
restoreMap(backupFile.syncMeta._String, RestoreSource.SYNC, restoreKeys)
restoreMap(backupFile.syncMeta._Float, RestoreSource.SYNC, restoreKeys)
restoreMap(backupFile.syncMeta._Long, RestoreSource.SYNC, restoreKeys)
restoreMap(backupFile.syncMeta._StringSet, RestoreSource.SYNC, restoreKeys)
for (restoreSource in restoreSources) {
val restoreData = RestoreMapData()
restoreData.addAll(backupFile.restore(this, restoreSource, restoreKeys))
// we must remove keys that are not present
if (!restoreKeys.isNullOrEmpty()) {
Log.d(BackupAPI.LOG_KEY, "successfulRestore for src=[${restoreSource.name}]: ${restoreData.successfulRestore}")
val removedKeys = restoreData.wantToRestore - restoreData.successfulRestore
Log.d(BackupAPI.LOG_KEY, "removed keys for src=[${restoreSource.name}]: $removedKeys")
removedKeys.forEach { removeKeyRaw(it, restoreSource) }
}
}
if (restoreSettings) {
restoreMap(backupFile.settings._Bool, RestoreSource.SETTINGS, restoreKeys)
restoreMap(backupFile.settings._Int, RestoreSource.SETTINGS, restoreKeys)
restoreMap(backupFile.settings._String, RestoreSource.SETTINGS, restoreKeys)
restoreMap(backupFile.settings._Float, RestoreSource.SETTINGS, restoreKeys)
restoreMap(backupFile.settings._Long, RestoreSource.SETTINGS, restoreKeys)
restoreMap(backupFile.settings._StringSet, RestoreSource.SETTINGS, restoreKeys)
}
if (restoreDataStore) {
restoreMap(backupFile.datastore._Bool, RestoreSource.DATA, restoreKeys)
restoreMap(backupFile.datastore._Int, RestoreSource.DATA, restoreKeys)
restoreMap(backupFile.datastore._String, RestoreSource.DATA, restoreKeys)
restoreMap(backupFile.datastore._Float, RestoreSource.DATA, restoreKeys)
restoreMap(backupFile.datastore._Long, RestoreSource.DATA, restoreKeys)
restoreMap(backupFile.datastore._StringSet, RestoreSource.DATA, restoreKeys)
}
Log.d(BackupAPI.LOG_KEY, "restore on ui event fired")
afterBackupRestoreEvent.invoke(Unit)
}
@SuppressLint("SimpleDateFormat")
@ -263,12 +298,7 @@ object BackupUtils {
val input = activity.contentResolver.openInputStream(uri)
?: return@ioSafe
activity.restore(
mapper.readValue(input),
restoreSettings = true,
restoreDataStore = true,
restoreSyncData = true
)
activity.restore(mapper.readValue(input))
activity.runOnUiThread { activity.recreate() }
} catch (e: Exception) {
logError(e)
@ -310,10 +340,10 @@ object BackupUtils {
private fun <T> Context.restoreMap(
map: Map<String, T>?,
restoreSource: RestoreSource,
restoreKeys: List<String>?
) {
val restoreOnlyThese = mutableListOf<String>()
val successfulRestore = mutableListOf<String>()
restoreKeys: Set<String>? = null
): RestoreMapData {
val restoreOnlyThese = mutableSetOf<String>()
val successfulRestore = mutableSetOf<String>()
if (!restoreKeys.isNullOrEmpty()) {
val prefixToMatch = "${BackupAPI.SYNC_HISTORY_PREFIX}${restoreSource.prefix}"
@ -327,26 +357,26 @@ object BackupUtils {
restoreOnlyThese.addAll(restore)
}
map?.filter {
var isTransferable = it.key.isTransferable()
var canRestore = isTransferable
if (restoreOnlyThese.isNotEmpty()) {
canRestore = canRestore && restoreOnlyThese.contains(it.key)
if (isTransferable && restoreOnlyThese.isNotEmpty()) {
isTransferable = restoreOnlyThese.contains(it.key)
}
if (isTransferable && canRestore) {
if (isTransferable) {
successfulRestore.add(it.key)
}
canRestore
isTransferable
}?.forEach {
setKeyRaw(it.key, it.value, restoreSource)
}
// we must remove keys that are not present
if (!restoreKeys.isNullOrEmpty()) {
var removedKeys = restoreOnlyThese - successfulRestore.toSet()
removedKeys.forEach { removeKeyRaw(it, restoreSource) }
}
return RestoreMapData(
restoreOnlyThese,
successfulRestore
)
}
}

View file

@ -27,7 +27,7 @@ object DataStore {
.configure(DeserializationFeature.USE_LONG_FOR_INTS, true)
.build()
private val backupScheduler = BackupAPI.createBackupScheduler()
private val backupScheduler = Scheduler.createBackupScheduler()
private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
@ -110,8 +110,11 @@ object DataStore {
editor.remove(path)
editor.apply()
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
backupScheduler.work(BackupAPI.PreferencesSchedulerData(path, false))
val success =
backupScheduler.work(BackupAPI.PreferencesSchedulerData(prefs, path, false))
if (success) {
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
}
}
} catch (e: Exception) {
logError(e)
@ -128,12 +131,15 @@ object DataStore {
fun <T> Context.setKey(path: String, value: T) {
try {
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
val prefs = getSharedPrefs()
val editor: SharedPreferences.Editor = prefs.edit()
editor.putString(path, mapper.writeValueAsString(value))
editor.apply()
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
backupScheduler.work(BackupAPI.PreferencesSchedulerData(path, false))
val success = backupScheduler.work(BackupAPI.PreferencesSchedulerData(prefs,path, false))
if (success) {
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
}
} catch (e: Exception) {
logError(e)
}

View file

@ -15,7 +15,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe

View file

@ -0,0 +1,127 @@
package com.lagradost.cloudstream3.utils
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.logHistoryChanged
import com.lagradost.cloudstream3.ui.player.PLAYBACK_SPEED_KEY
import com.lagradost.cloudstream3.ui.player.RESIZE_MODE_KEY
class Scheduler<INPUT>(
private val throttleTimeMs: Long,
private val onWork: (INPUT?) -> Unit,
private val canWork: ((INPUT?) -> Boolean)? = null
) {
companion object {
var SCHEDULER_ID = 1
fun createBackupScheduler() = Scheduler<BackupAPI.PreferencesSchedulerData>(
BackupAPI.UPLOAD_THROTTLE.inWholeMilliseconds,
onWork = { input ->
if (input == null) {
throw IllegalStateException()
}
AccountManager.BackupApis.forEach {
it.addToQueue(
input.storeKey,
input.isSettings
)
}
},
canWork = { input ->
if (input == null) {
throw IllegalStateException()
}
val invalidKeys = listOf(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
PLAYBACK_SPEED_KEY,
RESIZE_MODE_KEY
)
return@Scheduler !invalidKeys.contains(input.storeKey)
}
)
// Common usage is `val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().self`
// which means it is mostly used for settings preferences, therefore we use `isSettings: Boolean = true`, be careful
// if you need to directly access `context.getSharedPreferences` (without using DataStore) and dont forget to turn it off
fun SharedPreferences.attachBackupListener(
isSettings: Boolean = true,
syncPrefs: SharedPreferences
): BackupAPI.SharedPreferencesWithListener {
val scheduler = createBackupScheduler()
registerOnSharedPreferenceChangeListener { _, storeKey ->
val success =
scheduler.work(
BackupAPI.PreferencesSchedulerData(
syncPrefs,
storeKey,
isSettings
)
)
if (success) {
syncPrefs.logHistoryChanged(storeKey, BackupUtils.RestoreSource.SETTINGS)
}
}
return BackupAPI.SharedPreferencesWithListener(this, scheduler)
}
fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences): BackupAPI.SharedPreferencesWithListener {
return attachBackupListener(true, syncPrefs)
}
}
private val id = SCHEDULER_ID++
private val handler = Handler(Looper.getMainLooper())
private var runnable: Runnable? = null
fun work(input: INPUT? = null): Boolean {
if (canWork?.invoke(input) == false) {
// Log.d(LOG_KEY, "[$id] cannot schedule [${input}]")
return false
}
Log.d(BackupAPI.LOG_KEY, "[$id] wants to schedule [${input}]")
throttle(input)
return true
}
fun workNow(input: INPUT? = null): Boolean {
if (canWork?.invoke(input) == false) {
Log.d(BackupAPI.LOG_KEY, "[$id] cannot run immediate [${input}]")
return false
}
Log.d(BackupAPI.LOG_KEY, "[$id] runs immediate [${input}]")
stop()
onWork(input)
return true
}
fun stop() {
runnable?.let {
handler.removeCallbacks(it)
runnable = null
}
}
private fun throttle(input: INPUT?) {
stop()
runnable = Runnable {
Log.d(BackupAPI.LOG_KEY, "[$id] schedule success")
onWork(input)
}
handler.postDelayed(runnable!!, throttleTimeMs)
}
}