mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
feat: add remote sync capability - refactor and improve syncing when multiple devices are online
This commit is contained in:
parent
3d53ac6f88
commit
f4dfd2f5b9
18 changed files with 409 additions and 160 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,3 +14,4 @@
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
/.idea
|
|
@ -251,6 +251,7 @@ dependencies {
|
||||||
// color pallette for images -> colors
|
// color pallette for images -> colors
|
||||||
implementation("androidx.palette:palette-ktx:1.0.0")
|
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||||
|
|
||||||
|
implementation("org.skyscreamer:jsonassert:1.2.3")
|
||||||
implementation("androidx.browser:browser:1.4.0")
|
implementation("androidx.browser:browser:1.4.0")
|
||||||
implementation("com.google.api-client:google-api-client:2.0.0") {
|
implementation("com.google.api-client:google-api-client:2.0.0") {
|
||||||
exclude(
|
exclude(
|
||||||
|
|
|
@ -5,66 +5,211 @@ import android.content.SharedPreferences
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
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.seconds
|
||||||
|
|
||||||
interface BackupAPI<LOGIN_DATA> {
|
interface BackupAPI<LOGIN_DATA> {
|
||||||
|
data class JSONComparison(
|
||||||
|
val failed: Boolean,
|
||||||
|
val result: JSONCompareResult?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PreferencesSchedulerData(
|
||||||
|
val storeKey: String,
|
||||||
|
val isSettings: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SharedPreferencesWithListener(
|
||||||
|
val self: SharedPreferences,
|
||||||
|
val scheduler: Scheduler<PreferencesSchedulerData>
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val LOG_KEY = "BACKUP"
|
||||||
|
const val SYNC_HISTORY_PREFIX = "_hs/"
|
||||||
|
|
||||||
|
// Can be called in high frequency (for now) because current implementation uses google
|
||||||
|
// 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 = 10.seconds
|
val UPLOAD_THROTTLE = 10.seconds
|
||||||
val DOWNLOAD_THROTTLE = 60.seconds
|
val DOWNLOAD_THROTTLE = 60.seconds
|
||||||
|
|
||||||
fun createBackupScheduler() = Scheduler<Pair<String, Boolean>>(
|
// add to queue may be called frequently
|
||||||
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
fun createBackupScheduler() = Scheduler<PreferencesSchedulerData>(
|
||||||
UPLOAD_THROTTLE.inWholeMilliseconds
|
UPLOAD_THROTTLE.inWholeMilliseconds
|
||||||
) { input ->
|
) { input ->
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
throw IllegalStateException()
|
throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountManager.BackupApis.forEach { it.addToQueue(input.first, input.second) }
|
AccountManager.BackupApis.forEach { it.addToQueue(input.storeKey, input.isSettings) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SharedPreferences.attachListener(isSettings: Boolean = true): Pair<SharedPreferences, Scheduler<Pair<String, Boolean>>> {
|
// 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()
|
val scheduler = createBackupScheduler()
|
||||||
registerOnSharedPreferenceChangeListener { _, key ->
|
registerOnSharedPreferenceChangeListener { _, storeKey ->
|
||||||
scheduler.work(Pair(key, isSettings))
|
syncPrefs?.logHistoryChanged(storeKey, BackupUtils.RestoreSource.SETTINGS)
|
||||||
|
scheduler.work(PreferencesSchedulerData(storeKey, isSettings))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(
|
return SharedPreferencesWithListener(this, scheduler)
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should download data from API and call Context.mergeBackup(incomingData: String). If data
|
||||||
|
* does not exist on the api uploadSyncData() is recommended to call
|
||||||
|
* @see Context.mergeBackup
|
||||||
|
* @see uploadSyncData
|
||||||
|
*/
|
||||||
|
fun downloadSyncData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should upload data to API and call Context.createBackup(loginData: LOGIN_DATA)
|
||||||
|
* @see Context.createBackup(loginData: LOGIN_DATA)
|
||||||
|
*/
|
||||||
|
fun uploadSyncData()
|
||||||
|
|
||||||
|
|
||||||
|
fun Context.createBackup(loginData: LOGIN_DATA)
|
||||||
|
fun Context.mergeBackup(incomingData: String) {
|
||||||
|
val currentData = getBackup()
|
||||||
|
val newData = DataStore.mapper.readValue<BackupUtils.BackupFile>(incomingData)
|
||||||
|
|
||||||
|
val keysToUpdate = getKeysToUpdate(currentData, newData)
|
||||||
|
if (keysToUpdate.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(
|
||||||
|
newData,
|
||||||
|
keysToUpdate,
|
||||||
|
restoreSettings = true,
|
||||||
|
restoreDataStore = true,
|
||||||
|
restoreSyncData = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadSyncData()
|
|
||||||
fun uploadSyncData()
|
|
||||||
fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean
|
|
||||||
|
|
||||||
fun Context.mergeBackup(incomingData: String)
|
|
||||||
fun Context.createBackup(loginData: LOGIN_DATA)
|
|
||||||
|
|
||||||
var uploadJob: Job?
|
var uploadJob: Job?
|
||||||
|
fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean
|
||||||
fun addToQueue(changedKey: String, isSettings: Boolean) {
|
fun addToQueue(changedKey: String, isSettings: Boolean) {
|
||||||
if (!shouldUpdate(changedKey, isSettings)) {
|
if (!shouldUpdate(changedKey, isSettings)) {
|
||||||
Log.d("SYNC_API", "upload not required, data is same")
|
Log.d(LOG_KEY, "upload not required, data is same")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadJob != null && uploadJob!!.isActive) {
|
if (uploadJob != null && uploadJob!!.isActive) {
|
||||||
Log.d("SYNC_API", "upload is canceled, scheduling new")
|
Log.d(LOG_KEY, "upload is canceled, scheduling new")
|
||||||
uploadJob?.cancel()
|
uploadJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should ensure job will before app is closed
|
// we should ensure job will before app is closed
|
||||||
uploadJob = CoroutineScope(Dispatchers.IO).launchSafe {
|
uploadJob = ioScope.launchSafe {
|
||||||
Log.d("SYNC_API", "upload is running now")
|
Log.d(LOG_KEY, "upload is running now")
|
||||||
uploadSyncData()
|
uploadSyncData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun compareJson(old: String, new: String): JSONComparison {
|
||||||
|
var result: JSONCompareResult?
|
||||||
|
|
||||||
|
val executionTime = measureTimeMillis {
|
||||||
|
result = try {
|
||||||
|
JSONCompare.compareJSON(old, new, JSONCompareMode.LENIENT)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val failed = result?.failed() ?: true
|
||||||
|
Log.d(LOG_KEY, "JSON comparison took $executionTime ms, compareFailed=$failed")
|
||||||
|
|
||||||
|
return JSONComparison(failed, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
val changedKeys = newSync.filter {
|
||||||
|
val localTimestamp = if (currentSync[it.key] != null) {
|
||||||
|
currentSync[it.key]!!
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🤮
|
||||||
|
private fun getMissingKeys(
|
||||||
|
old: BackupUtils.BackupFile,
|
||||||
|
new: BackupUtils.BackupFile
|
||||||
|
): List<String> = mutableListOf(
|
||||||
|
*getMissing(old.settings._Bool, new.settings._Bool),
|
||||||
|
*getMissing(old.settings._Long, new.settings._Long),
|
||||||
|
*getMissing(old.settings._Float, new.settings._Float),
|
||||||
|
*getMissing(old.settings._Int, new.settings._Int),
|
||||||
|
*getMissing(old.settings._String, new.settings._String),
|
||||||
|
*getMissing(old.settings._StringSet, new.settings._StringSet),
|
||||||
|
*getMissing(old.datastore._Bool, new.datastore._Bool),
|
||||||
|
*getMissing(old.datastore._Long, new.datastore._Long),
|
||||||
|
*getMissing(old.datastore._Float, new.datastore._Float),
|
||||||
|
*getMissing(old.datastore._Int, new.datastore._Int),
|
||||||
|
*getMissing(old.datastore._String, new.datastore._String),
|
||||||
|
*getMissing(old.datastore._StringSet, new.datastore._StringSet),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getMissing(old: Map<String, *>?, new: Map<String, *>?): Array<String> =
|
||||||
|
new.orEmpty().keys.subtract(old.orEmpty().keys).toTypedArray()
|
||||||
|
|
||||||
class Scheduler<INPUT>(
|
class Scheduler<INPUT>(
|
||||||
private val throttleTimeMs: Long,
|
private val throttleTimeMs: Long,
|
||||||
|
@ -79,12 +224,12 @@ interface BackupAPI<LOGIN_DATA> {
|
||||||
private var runnable: Runnable? = null
|
private var runnable: Runnable? = null
|
||||||
|
|
||||||
fun work(input: INPUT? = null) {
|
fun work(input: INPUT? = null) {
|
||||||
Log.d("SYNC_API", "[$id] wants to schedule")
|
Log.d(LOG_KEY, "[$id] wants to schedule [${input}]")
|
||||||
throttle(input)
|
throttle(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun workNow(input: INPUT? = null) {
|
fun workNow(input: INPUT? = null) {
|
||||||
Log.d("SYNC_API", "[$id] runs immediate")
|
Log.d(LOG_KEY, "[$id] runs immediate [${input}]")
|
||||||
stop()
|
stop()
|
||||||
onWork(input)
|
onWork(input)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +245,7 @@ interface BackupAPI<LOGIN_DATA> {
|
||||||
stop()
|
stop()
|
||||||
|
|
||||||
runnable = Runnable {
|
runnable = Runnable {
|
||||||
Log.d("SYNC_API", "[$id] schedule success")
|
Log.d(LOG_KEY, "[$id] schedule success")
|
||||||
onWork(input)
|
onWork(input)
|
||||||
}
|
}
|
||||||
handler.postDelayed(runnable!!, throttleTimeMs)
|
handler.postDelayed(runnable!!, throttleTimeMs)
|
||||||
|
|
|
@ -5,13 +5,13 @@ import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow
|
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow
|
||||||
import com.google.api.client.auth.oauth2.Credential
|
import com.google.api.client.auth.oauth2.Credential
|
||||||
import com.google.api.client.auth.oauth2.TokenResponse
|
import com.google.api.client.auth.oauth2.TokenResponse
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
||||||
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
||||||
import com.google.api.client.http.FileContent
|
import com.google.api.client.http.FileContent
|
||||||
|
import com.google.api.client.http.javanet.NetHttpTransport
|
||||||
import com.google.api.client.json.gson.GsonFactory
|
import com.google.api.client.json.gson.GsonFactory
|
||||||
import com.google.api.client.util.store.MemoryDataStoreFactory
|
import com.google.api.client.util.store.MemoryDataStoreFactory
|
||||||
import com.google.api.services.drive.Drive
|
import com.google.api.services.drive.Drive
|
||||||
|
@ -22,28 +22,33 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.LOG_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2API
|
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2API
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2APIManager
|
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2APIManager
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils
|
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.getBackup
|
import com.lagradost.cloudstream3.utils.BackupUtils.getBackup
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.restore
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStore
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
// improvements and ideas
|
/**
|
||||||
// - add option to use proper oauth through google services one tap - would need google console project on behalf of cloudstream
|
* ## Improvements and ideas
|
||||||
// - encrypt data on drive - meh
|
*
|
||||||
// - choose what should be synced
|
* | State | Priority | Description
|
||||||
// - having a button to run sync would be nice - its really just for user to feel "safe"
|
* |---------:|:--------:|---------------------------------------------------------------------
|
||||||
// - having two or more devices in use at same time results in racing conditions
|
* | Progress | 2 | Restoring backup should update view models
|
||||||
// - restoring backup should update view models - maybe just first fragment is enough
|
* | Waiting | 2 | Add button to manually trigger sync
|
||||||
// - move "https://chiff.github.io/cloudstream-sync/google-drive" to cs3 repo
|
* | 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
|
||||||
|
* | 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
|
||||||
|
*/
|
||||||
class GoogleDriveApi(index: Int) :
|
class GoogleDriveApi(index: Int) :
|
||||||
InAppOAuth2APIManager(index),
|
InAppOAuth2APIManager(index),
|
||||||
BackupAPI<InAppOAuth2API.LoginData> {
|
BackupAPI<InAppOAuth2API.LoginData> {
|
||||||
|
@ -65,8 +70,8 @@ class GoogleDriveApi(index: Int) :
|
||||||
|
|
||||||
override var uploadJob: Job? = null
|
override var uploadJob: Job? = null
|
||||||
|
|
||||||
var tempAuthFlow: AuthorizationCodeFlow? = null
|
private var tempAuthFlow: AuthorizationCodeFlow? = null
|
||||||
var lastBackupJson: String? = null
|
private var lastBackupJson: String? = null
|
||||||
|
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
|
@ -163,49 +168,12 @@ class GoogleDriveApi(index: Int) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLatestLoginData(): InAppOAuth2API.LoginData? {
|
override fun getLatestLoginData(): InAppOAuth2API.LoginData? {
|
||||||
return getValue<InAppOAuth2API.LoginData>(K.LOGIN_DATA)
|
return getValue(K.LOGIN_DATA)
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
// BackupAPI implementation
|
// BackupAPI implementation
|
||||||
override fun Context.mergeBackup(incomingData: String) {
|
|
||||||
val currentData = getBackup()
|
|
||||||
val newData = DataStore.mapper.readValue<BackupUtils.BackupFile>(incomingData)
|
|
||||||
|
|
||||||
getRedundantKeys(currentData, newData).forEach {
|
|
||||||
removeKey(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
restore(
|
|
||||||
newData,
|
|
||||||
restoreSettings = true,
|
|
||||||
restoreDataStore = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🤮
|
|
||||||
private fun getRedundantKeys(
|
|
||||||
old: BackupUtils.BackupFile,
|
|
||||||
new: BackupUtils.BackupFile
|
|
||||||
): List<String> = mutableListOf(
|
|
||||||
*getRedundant(old.settings._Bool, new.settings._Bool),
|
|
||||||
*getRedundant(old.settings._Long, new.settings._Long),
|
|
||||||
*getRedundant(old.settings._Float, new.settings._Float),
|
|
||||||
*getRedundant(old.settings._Int, new.settings._Int),
|
|
||||||
*getRedundant(old.settings._String, new.settings._String),
|
|
||||||
*getRedundant(old.settings._StringSet, new.settings._StringSet),
|
|
||||||
*getRedundant(old.datastore._Bool, new.datastore._Bool),
|
|
||||||
*getRedundant(old.datastore._Long, new.datastore._Long),
|
|
||||||
*getRedundant(old.datastore._Float, new.datastore._Float),
|
|
||||||
*getRedundant(old.datastore._Int, new.datastore._Int),
|
|
||||||
*getRedundant(old.datastore._String, new.datastore._String),
|
|
||||||
*getRedundant(old.datastore._StringSet, new.datastore._StringSet),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getRedundant(old: Map<String, *>?, new: Map<String, *>?): Array<String> =
|
|
||||||
old.orEmpty().keys.subtract(new.orEmpty().keys).toTypedArray()
|
|
||||||
|
|
||||||
override fun Context.createBackup(loginData: InAppOAuth2API.LoginData) {
|
override fun Context.createBackup(loginData: InAppOAuth2API.LoginData) {
|
||||||
val drive = getDriveService() ?: return
|
val drive = getDriveService() ?: return
|
||||||
|
|
||||||
|
@ -263,13 +231,14 @@ class GoogleDriveApi(index: Int) :
|
||||||
try {
|
try {
|
||||||
val inputStream: InputStream = existingFile.executeMediaAsInputStream()
|
val inputStream: InputStream = existingFile.executeMediaAsInputStream()
|
||||||
val content: String = inputStream.bufferedReader().use { it.readText() }
|
val content: String = inputStream.bufferedReader().use { it.readText() }
|
||||||
Log.d("SYNC_API", "downloadSyncData merging")
|
Log.d(LOG_KEY, "downloadSyncData merging")
|
||||||
ctx.mergeBackup(content)
|
ctx.mergeBackup(content)
|
||||||
return
|
return
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_KEY,"download failed", e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d("SYNC_API", "downloadSyncData file not exists")
|
Log.d(LOG_KEY, "downloadSyncData file not exists")
|
||||||
uploadSyncData()
|
uploadSyncData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,16 +275,15 @@ class GoogleDriveApi(index: Int) :
|
||||||
override fun uploadSyncData() {
|
override fun uploadSyncData() {
|
||||||
val ctx = AcraApplication.context ?: return
|
val ctx = AcraApplication.context ?: return
|
||||||
val loginData = getLatestLoginData() ?: return
|
val loginData = getLatestLoginData() ?: return
|
||||||
Log.d("SYNC_API", "uploadSyncData createBackup")
|
Log.d(LOG_KEY, "uploadSyncData createBackup")
|
||||||
ctx.createBackup(loginData)
|
ctx.createBackup(loginData)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean {
|
override fun shouldUpdate(changedKey: String, isSettings: Boolean): Boolean {
|
||||||
val ctx = AcraApplication.context ?: return false
|
val ctx = AcraApplication.context ?: return false
|
||||||
|
|
||||||
// would be smarter to check properties, but its called once in UPLOAD_THROTTLE seconds
|
|
||||||
val newBackup = ctx.getBackup().toJson()
|
val newBackup = ctx.getBackup().toJson()
|
||||||
return lastBackupJson != newBackup
|
return compareJson(lastBackupJson ?: "", newBackup).failed
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDriveService(): Drive? {
|
private fun getDriveService(): Drive? {
|
||||||
|
@ -338,12 +306,14 @@ class GoogleDriveApi(index: Int) :
|
||||||
) {
|
) {
|
||||||
if (uploadJob?.isActive == true) {
|
if (uploadJob?.isActive == true) {
|
||||||
uploadJob!!.invokeOnCompletion {
|
uploadJob!!.invokeOnCompletion {
|
||||||
Log.d("SYNC_API", "upload is running, reschedule download")
|
Log.d(LOG_KEY, "upload is running, reschedule download")
|
||||||
runDownloader()
|
runDownloader()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d("SYNC_API", "downloadSyncData will run")
|
Log.d(LOG_KEY, "downloadSyncData will run")
|
||||||
|
ioSafe {
|
||||||
downloadSyncData()
|
downloadSyncData()
|
||||||
|
}
|
||||||
runDownloader()
|
runDownloader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,11 +328,11 @@ class GoogleDriveApi(index: Int) :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCredentialsFromStore(): Credential? {
|
private fun getCredentialsFromStore(): Credential? {
|
||||||
val LOGIN_DATA = getLatestLoginData()
|
val loginDate = getLatestLoginData()
|
||||||
val TOKEN = getValue<TokenResponse>(K.TOKEN)
|
val token = getValue<TokenResponse>(K.TOKEN)
|
||||||
|
|
||||||
val credential = if (LOGIN_DATA != null && TOKEN != null) {
|
val credential = if (loginDate != null && token != null) {
|
||||||
GAPI.getCredentials(TOKEN, LOGIN_DATA)
|
GAPI.getCredentials(token, loginDate)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -385,8 +355,8 @@ class GoogleDriveApi(index: Int) :
|
||||||
object GAPI {
|
object GAPI {
|
||||||
private const val DATA_STORE_ID = "gdrive_tokens"
|
private const val DATA_STORE_ID = "gdrive_tokens"
|
||||||
private val USED_SCOPES = listOf(DriveScopes.DRIVE_FILE)
|
private val USED_SCOPES = listOf(DriveScopes.DRIVE_FILE)
|
||||||
val HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport()
|
val HTTP_TRANSPORT: NetHttpTransport = GoogleNetHttpTransport.newTrustedTransport()
|
||||||
val JSON_FACTORY = GsonFactory.getDefaultInstance()
|
val JSON_FACTORY: GsonFactory = GsonFactory.getDefaultInstance()
|
||||||
|
|
||||||
fun createAuthFlow(clientId: String, clientSecret: String): GoogleAuthorizationCodeFlow =
|
fun createAuthFlow(clientId: String, clientSecret: String): GoogleAuthorizationCodeFlow =
|
||||||
GoogleAuthorizationCodeFlow.Builder(
|
GoogleAuthorizationCodeFlow.Builder(
|
||||||
|
|
|
@ -28,7 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.mvvm.*
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||||
|
@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAu
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.languages
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.languages
|
||||||
|
@ -56,8 +57,6 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_cl
|
||||||
import kotlinx.android.synthetic.main.player_select_tracks.*
|
import kotlinx.android.synthetic.main.player_select_tracks.*
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
class GeneratorPlayer : FullScreenPlayer() {
|
class GeneratorPlayer : FullScreenPlayer() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -665,7 +664,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceDialog.subtitles_click_settings?.setOnClickListener {
|
sourceDialog.subtitles_click_settings?.setOnClickListener {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||||
|
.attachBackupListener(ctx.getSyncPrefs()).self
|
||||||
|
|
||||||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||||
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
||||||
|
|
|
@ -25,11 +25,12 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.network.initClient
|
import com.lagradost.cloudstream3.network.initClient
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.EasterEggMonke
|
import com.lagradost.cloudstream3.ui.EasterEggMonke
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
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.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||||
|
@ -141,7 +142,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
||||||
// Stores the real URI using download_path_key
|
// Stores the real URI using download_path_key
|
||||||
// Important that the URI is stored instead of filepath due to permissions.
|
// Important that the URI is stored instead of filepath due to permissions.
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.attachListener().first
|
.attachBackupListener(context.getSyncPrefs()).self
|
||||||
.edit()
|
.edit()
|
||||||
.putString(getString(R.string.download_path_key), uri.toString())
|
.putString(getString(R.string.download_path_key), uri.toString())
|
||||||
.apply()
|
.apply()
|
||||||
|
@ -150,7 +151,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
||||||
// File path here is purely for cosmetic purposes in settings
|
// File path here is purely for cosmetic purposes in settings
|
||||||
(file.filePath ?: uri.toString()).let {
|
(file.filePath ?: uri.toString()).let {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.attachListener().first
|
.attachBackupListener(context.getSyncPrefs()).self
|
||||||
.edit()
|
.edit()
|
||||||
.putString(getString(R.string.download_path_pref), it)
|
.putString(getString(R.string.download_path_pref), it)
|
||||||
.apply()
|
.apply()
|
||||||
|
@ -160,7 +161,8 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
setPreferencesFromResource(R.xml.settins_general, rootKey)
|
setPreferencesFromResource(R.xml.settins_general, rootKey)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.attachBackupListener(requireContext().getSyncPrefs()).self
|
||||||
|
|
||||||
fun getCurrent(): MutableList<CustomSite> {
|
fun getCurrent(): MutableList<CustomSite> {
|
||||||
return getKey<Array<CustomSite>>(USER_PROVIDER_API)?.toMutableList()
|
return getKey<Array<CustomSite>>(USER_PROVIDER_API)?.toMutableList()
|
||||||
|
|
|
@ -7,13 +7,14 @@ import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
|
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.getPref
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
|
@ -28,7 +29,8 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
setPreferencesFromResource(R.xml.settings_player, rootKey)
|
setPreferencesFromResource(R.xml.settings_player, rootKey)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.attachBackupListener(requireContext().getSyncPrefs()).self
|
||||||
|
|
||||||
getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener {
|
getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener {
|
||||||
val prefNames = resources.getStringArray(R.array.video_buffer_length_names)
|
val prefNames = resources.getStringArray(R.array.video_buffer_length_names)
|
||||||
|
|
|
@ -6,20 +6,24 @@ import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.APIHolder
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
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.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
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.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
|
|
||||||
class SettingsProviders : PreferenceFragmentCompat() {
|
class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -31,7 +35,8 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
setPreferencesFromResource(R.xml.settings_providers, rootKey)
|
setPreferencesFromResource(R.xml.settings_providers, rootKey)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.attachBackupListener(requireContext().getSyncPrefs()).self
|
||||||
|
|
||||||
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
|
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
|
||||||
activity?.getApiDubstatusSettings()?.let { current ->
|
activity?.getApiDubstatusSettings()?.let { current ->
|
||||||
|
|
|
@ -8,12 +8,13 @@ import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.SearchQuality
|
import com.lagradost.cloudstream3.SearchQuality
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
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.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||||
|
@ -29,7 +30,8 @@ class SettingsUI : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
setPreferencesFromResource(R.xml.settins_ui, rootKey)
|
setPreferencesFromResource(R.xml.settins_ui, rootKey)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.attachBackupListener(requireContext().getSyncPrefs()).self
|
||||||
|
|
||||||
getPref(R.string.poster_ui_key)?.setOnPreferenceClickListener {
|
getPref(R.string.poster_ui_key)?.setOnPreferenceClickListener {
|
||||||
val prefNames = resources.getStringArray(R.array.poster_ui_options)
|
val prefNames = resources.getStringArray(R.array.poster_ui_options)
|
||||||
|
|
|
@ -14,19 +14,24 @@ import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
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.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import kotlinx.android.synthetic.main.logcat.*
|
import kotlinx.android.synthetic.main.logcat.clear_btt
|
||||||
|
import kotlinx.android.synthetic.main.logcat.close_btt
|
||||||
|
import kotlinx.android.synthetic.main.logcat.copy_btt
|
||||||
|
import kotlinx.android.synthetic.main.logcat.save_btt
|
||||||
|
import kotlinx.android.synthetic.main.logcat.text1
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
@ -127,7 +132,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPref(R.string.apk_installer_key)?.setOnPreferenceClickListener {
|
getPref(R.string.apk_installer_key)?.setOnPreferenceClickListener {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
|
||||||
|
.attachBackupListener(it.context.getSyncPrefs()).self
|
||||||
|
|
||||||
val prefNames = resources.getStringArray(R.array.apk_installer_pref)
|
val prefNames = resources.getStringArray(R.array.apk_installer_pref)
|
||||||
val prefValues = resources.getIntArray(R.array.apk_installer_values)
|
val prefValues = resources.getIntArray(R.array.apk_installer_values)
|
||||||
|
|
|
@ -15,12 +15,15 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.settings.appLanguages
|
import com.lagradost.cloudstream3.ui.settings.appLanguages
|
||||||
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
|
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_language.*
|
import kotlinx.android.synthetic.main.fragment_setup_language.app_icon_image
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_language.setup_root
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_language.skip_btt
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||||
|
|
||||||
|
@ -43,7 +46,8 @@ class SetupFragmentLanguage : Fragment() {
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
with(context) {
|
with(context) {
|
||||||
if (this == null) return@normalSafeApiCall
|
if (this == null) return@normalSafeApiCall
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
|
|
||||||
val arrayAdapter =
|
val arrayAdapter =
|
||||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||||
|
|
|
@ -10,9 +10,11 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_layout.*
|
import kotlinx.android.synthetic.main.fragment_setup_layout.acra_switch
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_layout.crash_reporting_text
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
||||||
|
@ -34,7 +36,8 @@ class SetupFragmentLayout : Fragment() {
|
||||||
|
|
||||||
with(context) {
|
with(context) {
|
||||||
if (this == null) return
|
if (this == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
|
|
||||||
val prefNames = resources.getStringArray(R.array.app_layout)
|
val prefNames = resources.getStringArray(R.array.app_layout)
|
||||||
val prefValues = resources.getIntArray(R.array.app_layout_values)
|
val prefValues = resources.getIntArray(R.array.app_layout_values)
|
||||||
|
|
|
@ -12,11 +12,15 @@ import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.*
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
|
||||||
|
|
||||||
|
|
||||||
class SetupFragmentMedia : Fragment() {
|
class SetupFragmentMedia : Fragment() {
|
||||||
|
@ -33,7 +37,8 @@ class SetupFragmentMedia : Fragment() {
|
||||||
|
|
||||||
with(context) {
|
with(context) {
|
||||||
if (this == null) return
|
if (this == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
|
|
||||||
val arrayAdapter =
|
val arrayAdapter =
|
||||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||||
|
|
|
@ -14,10 +14,14 @@ import com.lagradost.cloudstream3.APIHolder
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.AllLanguagesName
|
import com.lagradost.cloudstream3.AllLanguagesName
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import kotlinx.android.synthetic.main.fragment_setup_media.*
|
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
||||||
|
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
|
||||||
|
|
||||||
class SetupFragmentProviderLanguage : Fragment() {
|
class SetupFragmentProviderLanguage : Fragment() {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -34,7 +38,8 @@ class SetupFragmentProviderLanguage : Fragment() {
|
||||||
|
|
||||||
with(context) {
|
with(context) {
|
||||||
if (this == null) return
|
if (this == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
|
|
||||||
val arrayAdapter =
|
val arrayAdapter =
|
||||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||||
|
|
|
@ -27,8 +27,9 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
|
@ -448,7 +449,7 @@ class SubtitlesFragment : Fragment() {
|
||||||
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
|
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||||
.attachListener().first
|
.attachBackupListener(ctx.getSyncPrefs()).self
|
||||||
.edit()
|
.edit()
|
||||||
.putBoolean(getString(R.string.filter_sub_lang_key), b)
|
.putBoolean(getString(R.string.filter_sub_lang_key), b)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
|
@ -14,10 +14,12 @@ import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
|
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
|
||||||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
|
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2APIManager
|
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2APIManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
|
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY
|
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY
|
||||||
|
@ -33,7 +35,9 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
|
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.mapper
|
import com.lagradost.cloudstream3.utils.DataStore.mapper
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.removeKeyRaw
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw
|
import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||||
|
@ -46,6 +50,11 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object BackupUtils {
|
object BackupUtils {
|
||||||
|
enum class RestoreSource {
|
||||||
|
DATA, SETTINGS, SYNC;
|
||||||
|
|
||||||
|
val prefix = "$name/"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No sensitive or breaking data in the backup
|
* No sensitive or breaking data in the backup
|
||||||
|
@ -90,14 +99,25 @@ object BackupUtils {
|
||||||
|
|
||||||
data class BackupFile(
|
data class BackupFile(
|
||||||
@JsonProperty("datastore") val datastore: BackupVars,
|
@JsonProperty("datastore") val datastore: BackupVars,
|
||||||
@JsonProperty("settings") val settings: BackupVars
|
@JsonProperty("settings") val settings: BackupVars,
|
||||||
|
@JsonProperty("sync-meta") val syncMeta: BackupVars
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun Context.getBackup(): BackupFile {
|
fun Context.getBackup(): BackupFile {
|
||||||
|
val syncDataPrefs = getSyncPrefs().all.filter { it.key.isTransferable() }
|
||||||
val allData = getSharedPrefs().all.filter { it.key.isTransferable() }
|
val allData = getSharedPrefs().all.filter { it.key.isTransferable() }
|
||||||
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
||||||
|
|
||||||
|
val syncData = BackupVars(
|
||||||
|
syncDataPrefs.filter { it.value is Boolean } as? Map<String, Boolean>,
|
||||||
|
syncDataPrefs.filter { it.value is Int } as? Map<String, Int>,
|
||||||
|
syncDataPrefs.filter { it.value is String } as? Map<String, String>,
|
||||||
|
syncDataPrefs.filter { it.value is Float } as? Map<String, Float>,
|
||||||
|
syncDataPrefs.filter { it.value is Long } as? Map<String, Long>,
|
||||||
|
syncDataPrefs.filter { it.value as? Set<String> != null } as? Map<String, Set<String>>
|
||||||
|
)
|
||||||
|
|
||||||
val allDataSorted = BackupVars(
|
val allDataSorted = BackupVars(
|
||||||
allData.filter { it.value is Boolean } as? Map<String, Boolean>,
|
allData.filter { it.value is Boolean } as? Map<String, Boolean>,
|
||||||
allData.filter { it.value is Int } as? Map<String, Int>,
|
allData.filter { it.value is Int } as? Map<String, Int>,
|
||||||
|
@ -118,32 +138,44 @@ object BackupUtils {
|
||||||
|
|
||||||
return BackupFile(
|
return BackupFile(
|
||||||
allDataSorted,
|
allDataSorted,
|
||||||
allSettingsSorted
|
allSettingsSorted,
|
||||||
|
syncData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun Context.restore(
|
fun Context.restore(
|
||||||
backupFile: BackupFile,
|
backupFile: BackupFile,
|
||||||
|
restoreKeys: List<String>? = null,
|
||||||
restoreSettings: Boolean,
|
restoreSettings: Boolean,
|
||||||
restoreDataStore: Boolean
|
restoreDataStore: Boolean,
|
||||||
|
restoreSyncData: Boolean
|
||||||
) {
|
) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if (restoreSettings) {
|
if (restoreSettings) {
|
||||||
restoreMap(backupFile.settings._Bool, true)
|
restoreMap(backupFile.settings._Bool, RestoreSource.SETTINGS, restoreKeys)
|
||||||
restoreMap(backupFile.settings._Int, true)
|
restoreMap(backupFile.settings._Int, RestoreSource.SETTINGS, restoreKeys)
|
||||||
restoreMap(backupFile.settings._String, true)
|
restoreMap(backupFile.settings._String, RestoreSource.SETTINGS, restoreKeys)
|
||||||
restoreMap(backupFile.settings._Float, true)
|
restoreMap(backupFile.settings._Float, RestoreSource.SETTINGS, restoreKeys)
|
||||||
restoreMap(backupFile.settings._Long, true)
|
restoreMap(backupFile.settings._Long, RestoreSource.SETTINGS, restoreKeys)
|
||||||
restoreMap(backupFile.settings._StringSet, true)
|
restoreMap(backupFile.settings._StringSet, RestoreSource.SETTINGS, restoreKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoreDataStore) {
|
if (restoreDataStore) {
|
||||||
restoreMap(backupFile.datastore._Bool)
|
restoreMap(backupFile.datastore._Bool, RestoreSource.DATA, restoreKeys)
|
||||||
restoreMap(backupFile.datastore._Int)
|
restoreMap(backupFile.datastore._Int, RestoreSource.DATA, restoreKeys)
|
||||||
restoreMap(backupFile.datastore._String)
|
restoreMap(backupFile.datastore._String, RestoreSource.DATA, restoreKeys)
|
||||||
restoreMap(backupFile.datastore._Float)
|
restoreMap(backupFile.datastore._Float, RestoreSource.DATA, restoreKeys)
|
||||||
restoreMap(backupFile.datastore._Long)
|
restoreMap(backupFile.datastore._Long, RestoreSource.DATA, restoreKeys)
|
||||||
restoreMap(backupFile.datastore._StringSet)
|
restoreMap(backupFile.datastore._StringSet, RestoreSource.DATA, restoreKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,13 +264,11 @@ object BackupUtils {
|
||||||
val input = activity.contentResolver.openInputStream(uri)
|
val input = activity.contentResolver.openInputStream(uri)
|
||||||
?: return@ioSafe
|
?: return@ioSafe
|
||||||
|
|
||||||
val restoredValue =
|
|
||||||
mapper.readValue<BackupFile>(input)
|
|
||||||
|
|
||||||
activity.restore(
|
activity.restore(
|
||||||
restoredValue,
|
mapper.readValue(input),
|
||||||
restoreSettings = true,
|
restoreSettings = true,
|
||||||
restoreDataStore = true
|
restoreDataStore = true,
|
||||||
|
restoreSyncData = true
|
||||||
)
|
)
|
||||||
activity.runOnUiThread { activity.recreate() }
|
activity.runOnUiThread { activity.recreate() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -280,10 +310,44 @@ object BackupUtils {
|
||||||
|
|
||||||
private fun <T> Context.restoreMap(
|
private fun <T> Context.restoreMap(
|
||||||
map: Map<String, T>?,
|
map: Map<String, T>?,
|
||||||
isEditingAppSettings: Boolean = false
|
restoreSource: RestoreSource,
|
||||||
|
restoreKeys: List<String>?
|
||||||
) {
|
) {
|
||||||
map?.filter { it.key.isTransferable() }?.forEach {
|
val restoreOnlyThese = mutableListOf<String>()
|
||||||
setKeyRaw(it.key, it.value, isEditingAppSettings)
|
val successfulRestore = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (!restoreKeys.isNullOrEmpty()) {
|
||||||
|
val prefixToMatch = "${BackupAPI.SYNC_HISTORY_PREFIX}${restoreSource.prefix}"
|
||||||
|
|
||||||
|
val restore = restoreKeys.filter {
|
||||||
|
it.startsWith(prefixToMatch)
|
||||||
|
}.map {
|
||||||
|
it.removePrefix(prefixToMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreOnlyThese.addAll(restore)
|
||||||
|
}
|
||||||
|
|
||||||
|
map?.filter {
|
||||||
|
var isTransferable = it.key.isTransferable()
|
||||||
|
var canRestore = isTransferable
|
||||||
|
if (restoreOnlyThese.isNotEmpty()) {
|
||||||
|
canRestore = canRestore && restoreOnlyThese.contains(it.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTransferable && canRestore) {
|
||||||
|
successfulRestore.add(it.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
canRestore
|
||||||
|
}?.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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.logHistoryChanged
|
||||||
|
|
||||||
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
|
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ const val USER_SELECTED_HOMEPAGE_API = "home_api_used"
|
||||||
const val USER_PROVIDER_API = "user_custom_sites"
|
const val USER_PROVIDER_API = "user_custom_sites"
|
||||||
|
|
||||||
const val PREFERENCES_NAME = "rebuild_preference"
|
const val PREFERENCES_NAME = "rebuild_preference"
|
||||||
|
const val SYNC_PREFERENCES_NAME = "rebuild_sync_preference"
|
||||||
|
|
||||||
object DataStore {
|
object DataStore {
|
||||||
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
|
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
|
||||||
|
@ -31,6 +33,14 @@ object DataStore {
|
||||||
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSyncPreferences(context: Context): SharedPreferences {
|
||||||
|
return context.getSharedPreferences(SYNC_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getSyncPrefs(): SharedPreferences {
|
||||||
|
return getSyncPreferences(this)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.getSharedPrefs(): SharedPreferences {
|
fun Context.getSharedPrefs(): SharedPreferences {
|
||||||
return getPreferences(this)
|
return getPreferences(this)
|
||||||
}
|
}
|
||||||
|
@ -38,11 +48,14 @@ object DataStore {
|
||||||
fun getFolderName(folder: String, path: String): String {
|
fun getFolderName(folder: String, path: String): String {
|
||||||
return "${folder}/${path}"
|
return "${folder}/${path}"
|
||||||
}
|
}
|
||||||
|
fun <T> Context.setKeyRaw(path: String, value: T, restoreSource: BackupUtils.RestoreSource) {
|
||||||
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
|
|
||||||
try {
|
try {
|
||||||
val editor: SharedPreferences.Editor =
|
val editor = when (restoreSource) {
|
||||||
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
|
BackupUtils.RestoreSource.DATA -> getSharedPrefs().edit()
|
||||||
|
BackupUtils.RestoreSource.SETTINGS -> getDefaultSharedPrefs().edit()
|
||||||
|
BackupUtils.RestoreSource.SYNC -> getSyncPrefs().edit()
|
||||||
|
}
|
||||||
|
|
||||||
when (value) {
|
when (value) {
|
||||||
is Boolean -> editor.putBoolean(path, value)
|
is Boolean -> editor.putBoolean(path, value)
|
||||||
is Int -> editor.putInt(path, value)
|
is Int -> editor.putInt(path, value)
|
||||||
|
@ -56,6 +69,17 @@ object DataStore {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun Context.removeKeyRaw(path: String, restoreSource: BackupUtils.RestoreSource) {
|
||||||
|
try {
|
||||||
|
when (restoreSource) {
|
||||||
|
BackupUtils.RestoreSource.DATA -> getSharedPrefs().edit()
|
||||||
|
BackupUtils.RestoreSource.SETTINGS -> getDefaultSharedPrefs().edit()
|
||||||
|
BackupUtils.RestoreSource.SYNC -> getSyncPrefs().edit()
|
||||||
|
}.remove(path).apply()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.getDefaultSharedPrefs(): SharedPreferences {
|
fun Context.getDefaultSharedPrefs(): SharedPreferences {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
@ -85,7 +109,9 @@ object DataStore {
|
||||||
val editor: SharedPreferences.Editor = prefs.edit()
|
val editor: SharedPreferences.Editor = prefs.edit()
|
||||||
editor.remove(path)
|
editor.remove(path)
|
||||||
editor.apply()
|
editor.apply()
|
||||||
backupScheduler.work(Pair(path, false))
|
|
||||||
|
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
|
||||||
|
backupScheduler.work(BackupAPI.PreferencesSchedulerData(path, false))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -105,7 +131,9 @@ object DataStore {
|
||||||
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
|
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
|
||||||
editor.putString(path, mapper.writeValueAsString(value))
|
editor.putString(path, mapper.writeValueAsString(value))
|
||||||
editor.apply()
|
editor.apply()
|
||||||
backupScheduler.work(Pair(path, false))
|
|
||||||
|
getSyncPrefs().logHistoryChanged(path, BackupUtils.RestoreSource.DATA)
|
||||||
|
backupScheduler.work(BackupAPI.PreferencesSchedulerData(path, false))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -14,18 +15,18 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachBackupListener
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.io.File
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.attachListener
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
@ -74,7 +75,8 @@ class InAppUpdater {
|
||||||
|
|
||||||
private suspend fun Activity.getAppUpdate(): Update {
|
private suspend fun Activity.getAppUpdate(): Update {
|
||||||
return try {
|
return try {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
if (settingsManager.getBoolean(
|
if (settingsManager.getBoolean(
|
||||||
getString(R.string.prerelease_update_key),
|
getString(R.string.prerelease_update_key),
|
||||||
resources.getBoolean(R.bool.is_prerelease)
|
resources.getBoolean(R.bool.is_prerelease)
|
||||||
|
@ -255,7 +257,9 @@ class InAppUpdater {
|
||||||
* @param checkAutoUpdate if the update check was launched automatically
|
* @param checkAutoUpdate if the update check was launched automatically
|
||||||
**/
|
**/
|
||||||
suspend fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean {
|
suspend fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this).attachListener().first
|
val settingsManager =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.attachBackupListener(getSyncPrefs()).self
|
||||||
|
|
||||||
if (!checkAutoUpdate || settingsManager.getBoolean(
|
if (!checkAutoUpdate || settingsManager.getBoolean(
|
||||||
getString(R.string.auto_update_key),
|
getString(R.string.auto_update_key),
|
||||||
|
@ -265,7 +269,8 @@ class InAppUpdater {
|
||||||
val update = getAppUpdate()
|
val update = getAppUpdate()
|
||||||
if (
|
if (
|
||||||
update.shouldUpdate &&
|
update.shouldUpdate &&
|
||||||
update.updateURL != null) {
|
update.updateURL != null
|
||||||
|
) {
|
||||||
|
|
||||||
// Check if update should be skipped
|
// Check if update should be skipped
|
||||||
val updateNodeId =
|
val updateNodeId =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue