diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index b73105af..657ce58a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -44,7 +44,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used for active backup val BackupApis - get() = listOf>( + get() = listOf( googleDriveApi, pcloudApi ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt index 1a292195..a01d6d2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt @@ -7,9 +7,8 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.LOG_KEY -import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.compareJson -import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.mergeBackup +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.getBackup @@ -22,7 +21,7 @@ import org.skyscreamer.jsonassert.JSONCompare import org.skyscreamer.jsonassert.JSONCompareMode import org.skyscreamer.jsonassert.JSONCompareResult import kotlin.system.measureTimeMillis -import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.minutes interface RemoteFile { class Error(val message: String? = null, val throwable: Throwable? = null) : RemoteFile @@ -30,8 +29,83 @@ interface RemoteFile { class Success(val remoteData: String) : RemoteFile } +/** + * Safe wrapper for the backup api to be used outside the class without fear + * of causing crashes. + */ +interface SafeBackupAPI { + /** + * @return true if the service is ready for uploads and downloads. + * This includes a login check. + */ + suspend fun getIsReady(): Boolean + suspend fun scheduleDownload(runNow: Boolean = false) + fun getIsLoggedIn(): Boolean + fun scheduleUpload() + fun scheduleUpload(changedKey: String, isSettings: Boolean) + + /** + * Warns the service that an upload is incoming. Used to prevent simultaneous download and upload. + */ + fun setIsUploadingSoon() +} + +/** + * Easy interface to implement remote sync by only implementing the download and upload part of the service. + * @see BackupAPI for how the methods get used + */ +interface IBackupAPI { + /** + * Gets the user login info for uploading and downloading the backup. + * If null no backup or download will be run. + */ + suspend fun getLoginData(): LOGIN_DATA? + + /** + * Additional check if the backup operation should be run. + * Return false here to deny any backup work. + */ + suspend fun isReady(): Boolean = true + + /** + * Get the backup file as a string from the remote storage. + * @see RemoteFile.Success + * @see RemoteFile.Error + * @see RemoteFile.NotFound + */ + suspend fun getRemoteFile(context: Context, loginData: LOGIN_DATA): RemoteFile + + suspend fun uploadFile( + context: Context, + backupJson: String, + loginData: LOGIN_DATA + ) +} + +/** + * Helper class for the IBackupAPI which implements a scheduler, logging and checks. + * This makes the individual backup service implementations easier and more lightweight. + */ abstract class BackupAPI(defIndex: Int) : IBackupAPI, - AccountManager(defIndex) { + AccountManager(defIndex), SafeBackupAPI { + data class JSONComparison( + val failed: Boolean, + val result: JSONCompareResult? + ) + + data class PreferencesSchedulerData( + val syncPrefs: SharedPreferences, + val storeKey: String, + val oldValue: T, + val newValue: T, + val source: BackupUtils.RestoreSource + ) + + data class SharedPreferencesWithListener( + val self: SharedPreferences, + val scheduler: Scheduler> + ) + companion object { const val LOG_KEY = "BACKUP" @@ -39,8 +113,118 @@ abstract class BackupAPI(defIndex: Int) : IBackupAPI, // cloud project per user so there is no way to hit quota. Later we should implement // some kind of adaptive throttling which will increase decrease throttle time based // on factors like: live devices, quota limits, etc - val UPLOAD_THROTTLE = 30.seconds - val DOWNLOAD_THROTTLE = 120.seconds + val UPLOAD_THROTTLE = 5.minutes + val DOWNLOAD_THROTTLE = 5.minutes + + const val SYNC_HISTORY_PREFIX = "_hs/" + + fun SharedPreferences.logHistoryChanged( + path: String, + source: BackupUtils.RestoreSource + ) { + edit().putLong("${source.syncPrefix}$path", System.currentTimeMillis()).apply() + } + + fun compareJson(old: String, new: String): JSONComparison { + var result: JSONCompareResult? + + val executionTime = measureTimeMillis { + result = try { + JSONCompare.compareJSON(old, new, JSONCompareMode.NON_EXTENSIBLE) + } catch (e: Exception) { + null + } + } + + val failed = result?.failed() ?: true + Log.d( + LOG_KEY, + "JSON comparison took $executionTime ms, compareFailed=$failed, result=$result" + ) + + return JSONComparison(failed, result) + } + + private fun getSyncKeys(data: BackupUtils.BackupFile) = + data.syncMeta._Long.orEmpty().filter { it.key.startsWith(SYNC_HISTORY_PREFIX) } + + /** + * Merges the backup data with the app data. + * @param overwrite if true it overwrites all data same as restoring from a backup. + * if false it only updates outdated keys. Should be true on first initialization. + */ + private fun mergeBackup(context: Context, incomingData: String, overwrite: Boolean) { + val newData = DataStore.mapper.readValue(incomingData) + if (overwrite) { + Log.d(LOG_KEY, "overwriting data") + context.restore(newData) + + return + } + + val keysToUpdate = getKeysToUpdate(context.getBackup(), newData) + if (keysToUpdate.isEmpty()) { + Log.d(LOG_KEY, "remote data is up to date, sync not needed") + return + } + + Log.d(LOG_KEY, incomingData) + context.restore(newData, keysToUpdate) + } + + private fun getKeysToUpdate( + currentData: BackupUtils.BackupFile, + newData: BackupUtils.BackupFile + ): Set { + val currentSync = getSyncKeys(currentData) + val newSync = getSyncKeys(newData) + + val changedKeys = newSync.filter { + val localTimestamp = currentSync[it.key] ?: 0L + it.value > localTimestamp + }.keys + + val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) } + val missingKeys = getAllMissingKeys(currentData, newData) + + return (missingKeys + onlyLocalKeys + changedKeys).toSet() + } + + private fun getAllMissingKeys( + old: BackupUtils.BackupFile, + new: BackupUtils.BackupFile + ): List = BackupUtils.RestoreSource + .values() + .filter { it != BackupUtils.RestoreSource.SYNC } + .fold(mutableListOf()) { acc, source -> + acc.addAll(getMissingKeysPrefixed(source, old, new)) + acc + } + + private fun getMissingKeysPrefixed( + restoreSource: BackupUtils.RestoreSource, + old: BackupUtils.BackupFile, + new: BackupUtils.BackupFile + ): List { + val oldSource = old.getData(restoreSource) + val newSource = new.getData(restoreSource) + val prefixToMatch = restoreSource.syncPrefix + + return listOf( + *getMissing(oldSource._Bool, newSource._Bool), + *getMissing(oldSource._Long, newSource._Long), + *getMissing(oldSource._Float, newSource._Float), + *getMissing(oldSource._Int, newSource._Int), + *getMissing(oldSource._String, newSource._String), + *getMissing(oldSource._StringSet, newSource._StringSet), + ).map { + prefixToMatch + it + } + } + + + private fun getMissing(old: Map?, new: Map?): Array = + (new.orEmpty().keys - old.orEmpty().keys).toTypedArray() } /** @@ -77,7 +261,7 @@ abstract class BackupAPI(defIndex: Int) : IBackupAPI, } } - var willUploadSoon: Boolean? = null + private var willUploadSoon: Boolean? = null private var uploadJob: Job? = null private fun shouldUploadBackup(): Boolean { @@ -87,19 +271,23 @@ abstract class BackupAPI(defIndex: Int) : IBackupAPI, return compareJson(lastBackupJson ?: "", newBackup).failed } - fun scheduleUpload() { - if (!shouldUploadBackup()) { - willUploadSoon = false - Log.d(LOG_KEY, "${this.name}: upload not required, data is same") - return - } + override fun scheduleUpload() { + normalSafeApiCall { + if (!shouldUploadBackup()) { + willUploadSoon = false + Log.d(LOG_KEY, "${this.name}: upload not required, data is same") + return@normalSafeApiCall + } - upload() + upload() + } } // changedKey and isSettings is currently unused, might be useful for more efficient update checker. - fun scheduleUpload(changedKey: String, isSettings: Boolean) { - scheduleUpload() + override fun scheduleUpload(changedKey: String, isSettings: Boolean) { + normalSafeApiCall { + scheduleUpload() + } } private fun upload() { @@ -170,7 +358,10 @@ abstract class BackupAPI(defIndex: Int) : IBackupAPI, } is RemoteFile.Error -> { - Log.d(LOG_KEY, "${this.name}: getRemoteFile failed with message: ${remoteFile.message}.") + Log.d( + LOG_KEY, + "${this.name}: getRemoteFile failed with message: ${remoteFile.message}." + ) remoteFile.throwable?.let { error -> logError(error) } null } @@ -195,162 +386,26 @@ abstract class BackupAPI(defIndex: Int) : IBackupAPI, lastBackupJson = remoteData mergeBackup(context, remoteData, overwrite) } -} - -interface IBackupAPI { - data class JSONComparison( - val failed: Boolean, - val result: JSONCompareResult? - ) - - data class PreferencesSchedulerData( - val syncPrefs: SharedPreferences, - val storeKey: String, - val oldValue: T, - val newValue: T, - val source: BackupUtils.RestoreSource - ) - - data class SharedPreferencesWithListener( - val self: SharedPreferences, - val scheduler: Scheduler> - ) - - /** - * Gets the user login info for uploading and downloading the backup. - * If null no backup or download will be run. - */ - suspend fun getLoginData(): LOGIN_DATA? - - /** - * Additional check if the backup operation should be run. - * Return false here to deny any backup work. - */ - suspend fun isReady(): Boolean = true - - /** - * Get the backup file as a string from the remote storage. - * @see RemoteFile.Success - * @see RemoteFile.Error - * @see RemoteFile.NotFound - */ - suspend fun getRemoteFile(context: Context, loginData: LOGIN_DATA): RemoteFile - - suspend fun uploadFile( - context: Context, - backupJson: String, - loginData: LOGIN_DATA - ) - - companion object { - const val SYNC_HISTORY_PREFIX = "_hs/" - - fun SharedPreferences.logHistoryChanged(path: String, source: BackupUtils.RestoreSource) { - edit().putLong("${source.syncPrefix}$path", System.currentTimeMillis()) - .apply() - } - - fun compareJson(old: String, new: String): JSONComparison { - var result: JSONCompareResult? - - val executionTime = measureTimeMillis { - result = try { - JSONCompare.compareJSON(old, new, JSONCompareMode.NON_EXTENSIBLE) - } catch (e: Exception) { - null - } - } - - val failed = result?.failed() ?: true - Log.d( - LOG_KEY, - "JSON comparison took $executionTime ms, compareFailed=$failed, result=$result" - ) - - return JSONComparison(failed, result) - } - - private fun getSyncKeys(data: BackupUtils.BackupFile) = - data.syncMeta._Long.orEmpty().filter { it.key.startsWith(SYNC_HISTORY_PREFIX) } - - /** - * Merges the backup data with the app data. - * @param overwrite if true it overwrites all data same as restoring from a backup. - * if false it only updates outdated keys. Should be true on first initialization. - */ - fun mergeBackup(context: Context, incomingData: String, overwrite: Boolean) { - val newData = DataStore.mapper.readValue(incomingData) - if (overwrite) { - Log.d(LOG_KEY, "overwriting data") - context.restore(newData) - - return - } - - val keysToUpdate = getKeysToUpdate(context.getBackup(), newData) - if (keysToUpdate.isEmpty()) { - Log.d(LOG_KEY, "remote data is up to date, sync not needed") - return - } - Log.d(LOG_KEY, incomingData) - context.restore(newData, keysToUpdate) - } - - private fun getKeysToUpdate( - currentData: BackupUtils.BackupFile, - newData: BackupUtils.BackupFile - ): Set { - val currentSync = getSyncKeys(currentData) - val newSync = getSyncKeys(newData) - - val changedKeys = newSync.filter { - val localTimestamp = currentSync[it.key] ?: 0L - it.value > localTimestamp - }.keys - - val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) } - val missingKeys = getAllMissingKeys(currentData, newData) - - return (missingKeys + onlyLocalKeys + changedKeys).toSet() - } - - private fun getAllMissingKeys( - old: BackupUtils.BackupFile, - new: BackupUtils.BackupFile - ): List = BackupUtils.RestoreSource - .values() - .filter { it != BackupUtils.RestoreSource.SYNC } - .fold(mutableListOf()) { acc, source -> - acc.addAll(getMissingKeysPrefixed(source, old, new)) - acc - } - - private fun getMissingKeysPrefixed( - restoreSource: BackupUtils.RestoreSource, - old: BackupUtils.BackupFile, - new: BackupUtils.BackupFile - ): List { - val oldSource = old.getData(restoreSource) - val newSource = new.getData(restoreSource) - val prefixToMatch = restoreSource.syncPrefix - - return listOf( - *getMissing(oldSource._Bool, newSource._Bool), - *getMissing(oldSource._Long, newSource._Long), - *getMissing(oldSource._Float, newSource._Float), - *getMissing(oldSource._Int, newSource._Int), - *getMissing(oldSource._String, newSource._String), - *getMissing(oldSource._StringSet, newSource._StringSet), - ).map { - prefixToMatch + it - } - } - - - private fun getMissing(old: Map?, new: Map?): Array = - (new.orEmpty().keys - old.orEmpty().keys) - .toTypedArray() + // ------ SafeBackupAPI wrappers ------ + override suspend fun getIsReady(): Boolean { + return suspendSafeApiCall { + isReady() + } ?: false } -} + + override fun getIsLoggedIn(): Boolean { + return normalSafeApiCall { loginInfo() } != null + } + + override fun setIsUploadingSoon() { + willUploadSoon = true + } + + override suspend fun scheduleDownload(runNow: Boolean) { + suspendSafeApiCall { + scheduleDownload(runNow, false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GoogleDriveApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GoogleDriveApi.kt index f0601be1..2ca37ed2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GoogleDriveApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GoogleDriveApi.kt @@ -85,9 +85,9 @@ class GoogleDriveApi(index: Int) : accountId, key.value, value ) - private fun clearValue(key: K) = removeKey(accountId, key.value) + private fun clearValue(key: K) = removeKey(accountId, key.value) - private inline fun getValue(key: K) = getKey( + private inline fun getValue(key: K) = getKey( accountId, key.value ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 4295ff5d..4581dcb2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.googleDriveApi @@ -150,7 +151,7 @@ class SettingsAccount : PreferenceFragmentCompat() { title = getString(R.string.login_format).format(api.name, getString(R.string.account)) setOnPreferenceClickListener { - val info = api.loginInfo() + val info = normalSafeApiCall { api.loginInfo() } if (info != null) { showLoginInfo(activity, api, info) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index eb5b2401..6a5a2f62 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -24,9 +24,9 @@ import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers -import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -62,7 +62,8 @@ class SettingsFragment : Fragment() { fun Fragment?.setUpToolbar(title: String) { if (this == null) return - val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + val settingsToolbar = + view?.findViewById(R.id.settings_toolbar) ?: return settingsToolbar.apply { setTitle(title) @@ -76,7 +77,8 @@ class SettingsFragment : Fragment() { fun Fragment?.setUpToolbar(@StringRes title: Int) { if (this == null) return - val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + val settingsToolbar = + view?.findViewById(R.id.settings_toolbar) ?: return settingsToolbar.apply { setTitle(title) @@ -213,7 +215,7 @@ class SettingsFragment : Fragment() { // Only show the button if the api does not require login, requires login, but the user is logged in forceSyncDataBtt.isVisible = BackupApis.any { api -> - api !is AuthAPI || api.loginInfo() != null + api.getIsLoggedIn() } forceSyncDataBtt.setOnClickListener { @@ -223,7 +225,8 @@ class SettingsFragment : Fragment() { showToast(activity, txt(R.string.syncing_data), Toast.LENGTH_SHORT) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - forceSyncDataBtt.tooltipText = txt(R.string.sync_data).asString(forceSyncDataBtt.context) + forceSyncDataBtt.tooltipText = + txt(R.string.sync_data).asString(forceSyncDataBtt.context) } // Default focus on TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index f75e8315..eea106c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -54,7 +54,7 @@ object BackupUtils { DATA, SETTINGS, SYNC; val prefix = "$name/" - val syncPrefix = "${IBackupAPI.SYNC_HISTORY_PREFIX}$prefix" + val syncPrefix = "${BackupAPI.SYNC_HISTORY_PREFIX}$prefix" } /** @@ -80,7 +80,6 @@ object BackupUtils { OPEN_SUBTITLES_USER_KEY, "nginx_user", // Nginx user key - DOWNLOAD_HEADER_CACHE, DOWNLOAD_EPISODE_CACHE ) @@ -329,7 +328,7 @@ object BackupUtils { var prefixToRemove = prefixToMatch if (restoreSource == RestoreSource.SYNC) { - prefixToMatch = IBackupAPI.SYNC_HISTORY_PREFIX + prefixToMatch = BackupAPI.SYNC_HISTORY_PREFIX prefixToRemove = "" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 1c99f0bf..66a62034 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -10,9 +10,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKeyClass import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.BackupAPI import kotlin.reflect.KClass import kotlin.reflect.KProperty -import com.lagradost.cloudstream3.syncproviders.IBackupAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -149,7 +149,7 @@ object DataStore { ioSafe { backupScheduler.work( - IBackupAPI.PreferencesSchedulerData( + BackupAPI.PreferencesSchedulerData( getSyncPrefs(), path, oldValueExists, @@ -184,7 +184,7 @@ object DataStore { ioSafe { backupScheduler.work( - IBackupAPI.PreferencesSchedulerData( + BackupAPI.PreferencesSchedulerData( getSyncPrefs(), path, oldValue, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt index 3bc6ec6d..319f09cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Scheduler.kt @@ -6,14 +6,12 @@ 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.IBackupAPI -import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.logHistoryChanged +import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.logHistoryChanged import com.lagradost.cloudstream3.ui.home.HOME_BOOKMARK_VALUE_LIST import com.lagradost.cloudstream3.ui.player.PLAYBACK_SPEED_KEY import com.lagradost.cloudstream3.ui.player.RESIZE_MODE_KEY import com.lagradost.cloudstream3.utils.BackupUtils.nonTransferableKeys import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import kotlinx.coroutines.runBlocking class Scheduler( private val throttleTimeMs: Long, @@ -40,7 +38,7 @@ class Scheduler( Regex("""^\d+/$RESULT_DUB/"""), ) - fun createBackupScheduler() = Scheduler>( + fun createBackupScheduler() = Scheduler>( BackupAPI.UPLOAD_THROTTLE.inWholeMilliseconds, onWork = { input -> AccountManager.BackupApis.forEach { api -> @@ -52,13 +50,13 @@ class Scheduler( }, beforeWork = { _ -> AccountManager.BackupApis.filter { api -> - api.isReady() + api.getIsReady() }.forEach { - it.willUploadSoon = true + it.setIsUploadingSoon() } }, canWork = { input -> - val hasSomeActiveManagers = AccountManager.BackupApis.any { it.isReady() } + val hasSomeActiveManagers = AccountManager.BackupApis.any { it.getIsReady() } if (!hasSomeActiveManagers) { return@Scheduler false } @@ -97,14 +95,14 @@ class Scheduler( fun SharedPreferences.attachBackupListener( source: BackupUtils.RestoreSource = BackupUtils.RestoreSource.SETTINGS, syncPrefs: SharedPreferences - ): IBackupAPI.SharedPreferencesWithListener { + ): BackupAPI.SharedPreferencesWithListener { val scheduler = createBackupScheduler() var lastValue = all registerOnSharedPreferenceChangeListener { sharedPreferences, storeKey -> ioSafe { scheduler.work( - IBackupAPI.PreferencesSchedulerData( + BackupAPI.PreferencesSchedulerData( syncPrefs, storeKey, lastValue[storeKey], @@ -116,10 +114,10 @@ class Scheduler( lastValue = sharedPreferences.all } - return IBackupAPI.SharedPreferencesWithListener(this, scheduler) + return BackupAPI.SharedPreferencesWithListener(this, scheduler) } - fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences): IBackupAPI.SharedPreferencesWithListener { + fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences): BackupAPI.SharedPreferencesWithListener { return attachBackupListener(BackupUtils.RestoreSource.SETTINGS, syncPrefs) } } @@ -172,7 +170,7 @@ class Scheduler( runnable = Runnable { Log.d(BackupAPI.LOG_KEY, "[$id] schedule success") - runBlocking { + ioSafe { onWork(input) } }.also { run ->