mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
feat: add remote sync capability - refactor, refresh ui on restore and improve data restore (pt.2)
This commit is contained in:
parent
e1e039b58c
commit
fe36b69758
21 changed files with 363 additions and 227 deletions
|
@ -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>()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
127
app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt
Normal file
127
app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue