566 lines
21 KiB
Kotlin
566 lines
21 KiB
Kotlin
package com.lagradost.cloudstream3.utils
|
|
|
|
import android.content.Context
|
|
import android.content.DialogInterface
|
|
import android.text.Editable
|
|
import android.view.LayoutInflater
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.core.view.isGone
|
|
import androidx.core.widget.doOnTextChanged
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
import com.lagradost.cloudstream3.*
|
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
|
import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountEditBinding
|
|
import com.lagradost.cloudstream3.databinding.WhoIsWatchingBinding
|
|
import com.lagradost.cloudstream3.mvvm.logError
|
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
import com.lagradost.cloudstream3.ui.WatchType
|
|
import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter
|
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
|
import com.lagradost.cloudstream3.ui.result.UiImage
|
|
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
|
import com.lagradost.cloudstream3.ui.result.setImage
|
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
|
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|
|
|
const val VIDEO_POS_DUR = "video_pos_dur"
|
|
const val VIDEO_WATCH_STATE = "video_watch_state"
|
|
const val RESULT_WATCH_STATE = "result_watch_state"
|
|
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
|
|
const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data"
|
|
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
|
|
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
|
|
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
|
|
const val RESULT_EPISODE = "result_episode"
|
|
const val RESULT_SEASON = "result_season"
|
|
const val RESULT_DUB = "result_dub"
|
|
|
|
object DataStoreHelper {
|
|
// be aware, don't change the index of these as Account uses the index for the art
|
|
private val profileImages = arrayOf(
|
|
R.drawable.profile_bg_dark_blue,
|
|
R.drawable.profile_bg_blue,
|
|
R.drawable.profile_bg_orange,
|
|
R.drawable.profile_bg_pink,
|
|
R.drawable.profile_bg_purple,
|
|
R.drawable.profile_bg_red,
|
|
R.drawable.profile_bg_teal
|
|
)
|
|
|
|
data class Account(
|
|
@JsonProperty("keyIndex")
|
|
val keyIndex: Int,
|
|
@JsonProperty("name")
|
|
val name: String,
|
|
@JsonProperty("customImage")
|
|
val customImage: String? = null,
|
|
@JsonProperty("defaultImageIndex")
|
|
val defaultImageIndex: Int,
|
|
) {
|
|
val image: UiImage
|
|
get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable(
|
|
profileImages.getOrNull(defaultImageIndex) ?: profileImages.first()
|
|
)
|
|
}
|
|
|
|
const val TAG = "data_store_helper"
|
|
private var accounts by PreferenceDelegate("$TAG/account", arrayOf<Account>())
|
|
var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0)
|
|
val currentAccount: String get() = selectedKeyIndex.toString()
|
|
|
|
private fun setAccount(account: Account) {
|
|
selectedKeyIndex = account.keyIndex
|
|
showToast(account.name)
|
|
MainActivity.bookmarksUpdatedEvent(true)
|
|
}
|
|
|
|
private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) {
|
|
val binding =
|
|
WhoIsWatchingAccountEditBinding.inflate(LayoutInflater.from(context), null, false)
|
|
val builder =
|
|
AlertDialog.Builder(context, R.style.AlertDialogCustom)
|
|
.setView(binding.root)
|
|
|
|
var currentEditAccount = account
|
|
val dialog = builder.show()
|
|
binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name)
|
|
binding.accountName.doOnTextChanged { text, _, _, _ ->
|
|
currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "")
|
|
}
|
|
|
|
binding.deleteBtt.isGone = isNewAccount
|
|
binding.deleteBtt.setOnClickListener {
|
|
val dialogClickListener =
|
|
DialogInterface.OnClickListener { _, which ->
|
|
when (which) {
|
|
DialogInterface.BUTTON_POSITIVE -> {
|
|
// remove all keys as well as the account, note that default wont get
|
|
// deleted from currentAccounts, as it is not part of "accounts",
|
|
// but the watch keys will
|
|
removeKeys(account.keyIndex.toString())
|
|
val currentAccounts = accounts.toMutableList()
|
|
currentAccounts.removeIf { it.keyIndex == account.keyIndex }
|
|
accounts = currentAccounts.toTypedArray()
|
|
|
|
// update UI
|
|
setAccount(getDefaultAccount(context))
|
|
MainActivity.bookmarksUpdatedEvent(true)
|
|
dialog?.dismissSafe()
|
|
}
|
|
|
|
DialogInterface.BUTTON_NEGATIVE -> {}
|
|
}
|
|
}
|
|
|
|
try {
|
|
AlertDialog.Builder(context).setTitle(R.string.delete).setMessage(
|
|
context.getString(R.string.delete_message).format(
|
|
currentEditAccount.name
|
|
)
|
|
)
|
|
.setPositiveButton(R.string.delete, dialogClickListener)
|
|
.setNegativeButton(R.string.cancel, dialogClickListener)
|
|
.show().setDefaultFocus()
|
|
} catch (t: Throwable) {
|
|
logError(t)
|
|
// ye you somehow fucked up formatting did you?
|
|
}
|
|
}
|
|
|
|
binding.cancelBtt.setOnClickListener {
|
|
dialog?.dismissSafe()
|
|
}
|
|
|
|
binding.profilePic.setImage(account.image)
|
|
binding.profilePic.setOnClickListener {
|
|
// rolls the image forwards once
|
|
currentEditAccount =
|
|
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
|
|
binding.profilePic.setImage(currentEditAccount.image)
|
|
}
|
|
|
|
binding.applyBtt.setOnClickListener {
|
|
val currentAccounts = accounts.toMutableList()
|
|
|
|
val overrideIndex =
|
|
currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex }
|
|
|
|
// if an account is found that has the same keyIndex then override that one, if not then append it
|
|
if (overrideIndex != -1) {
|
|
currentAccounts[overrideIndex] = currentEditAccount
|
|
} else {
|
|
currentAccounts.add(currentEditAccount)
|
|
}
|
|
|
|
// set the new default account as well as add the key for the new account
|
|
setAccount(currentEditAccount)
|
|
accounts = currentAccounts.toTypedArray()
|
|
|
|
dialog.dismissSafe()
|
|
}
|
|
}
|
|
|
|
private fun getDefaultAccount(context: Context): Account {
|
|
return accounts.let { currentAccounts ->
|
|
currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account(
|
|
keyIndex = 0,
|
|
name = context.getString(R.string.default_account),
|
|
defaultImageIndex = 0
|
|
)
|
|
}
|
|
}
|
|
|
|
fun showWhoIsWatching(context: Context) {
|
|
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(
|
|
LayoutInflater.from(context)
|
|
)
|
|
|
|
val showAccount = accounts.toMutableList().apply {
|
|
val item = getDefaultAccount(context)
|
|
remove(item)
|
|
add(0, item)
|
|
}
|
|
|
|
val builder =
|
|
BottomSheetDialog(context)
|
|
builder.setContentView(binding.root)
|
|
val accountName = context.getString(R.string.account)
|
|
|
|
binding.profilesRecyclerview.setLinearListLayout(
|
|
isHorizontal = true,
|
|
nextUp = FOCUS_SELF,
|
|
nextDown = FOCUS_SELF,
|
|
nextLeft = FOCUS_SELF,
|
|
nextRight = FOCUS_SELF
|
|
)
|
|
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
|
|
selectCallBack = { account ->
|
|
setAccount(account)
|
|
builder.dismissSafe()
|
|
},
|
|
addAccountCallback = {
|
|
val currentAccounts = accounts
|
|
val remainingImages =
|
|
profileImages.toSet() - currentAccounts.filter { it.customImage == null }
|
|
.mapNotNull { profileImages.getOrNull(it.defaultImageIndex) }.toSet()
|
|
val image =
|
|
profileImages.indexOf(remainingImages.randomOrNull() ?: profileImages.random())
|
|
val keyIndex = (currentAccounts.maxOfOrNull { it.keyIndex } ?: 0) + 1
|
|
|
|
// create a new dummy account
|
|
editAccount(
|
|
context,
|
|
Account(
|
|
keyIndex = keyIndex,
|
|
name = "$accountName $keyIndex",
|
|
customImage = null,
|
|
defaultImageIndex = image
|
|
), isNewAccount = true
|
|
)
|
|
builder.dismissSafe()
|
|
},
|
|
editCallBack = { account ->
|
|
editAccount(
|
|
context, account, isNewAccount = false
|
|
)
|
|
builder.dismissSafe()
|
|
}
|
|
).apply {
|
|
submitList(showAccount)
|
|
}
|
|
|
|
builder.show()
|
|
}
|
|
|
|
|
|
data class PosDur(
|
|
@JsonProperty("position") val position: Long,
|
|
@JsonProperty("duration") val duration: Long
|
|
)
|
|
|
|
fun PosDur.fixVisual(): PosDur {
|
|
if (duration <= 0) return PosDur(0, duration)
|
|
val percentage = position * 100 / duration
|
|
if (percentage <= 1) return PosDur(0, duration)
|
|
if (percentage <= 5) return PosDur(5 * duration / 100, duration)
|
|
if (percentage >= 95) return PosDur(duration, duration)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Used to display notifications on new episodes and posters in library.
|
|
**/
|
|
data class SubscribedData(
|
|
@JsonProperty("id") override var id: Int?,
|
|
@JsonProperty("subscribedTime") val bookmarkedTime: Long,
|
|
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
|
|
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
|
|
@JsonProperty("name") override val name: String,
|
|
@JsonProperty("url") override val url: String,
|
|
@JsonProperty("apiName") override val apiName: String,
|
|
@JsonProperty("type") override var type: TvType? = null,
|
|
@JsonProperty("posterUrl") override var posterUrl: String?,
|
|
@JsonProperty("year") val year: Int?,
|
|
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
|
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
|
) : SearchResponse {
|
|
fun toLibraryItem(): SyncAPI.LibraryItem? {
|
|
return SyncAPI.LibraryItem(
|
|
name,
|
|
url,
|
|
id?.toString() ?: return null,
|
|
null,
|
|
null,
|
|
null,
|
|
latestUpdatedTime,
|
|
apiName, type, posterUrl, posterHeaders, quality, this.id
|
|
)
|
|
}
|
|
}
|
|
|
|
data class BookmarkedData(
|
|
@JsonProperty("id") override var id: Int?,
|
|
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
|
|
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
|
|
@JsonProperty("name") override val name: String,
|
|
@JsonProperty("url") override val url: String,
|
|
@JsonProperty("apiName") override val apiName: String,
|
|
@JsonProperty("type") override var type: TvType? = null,
|
|
@JsonProperty("posterUrl") override var posterUrl: String?,
|
|
@JsonProperty("year") val year: Int?,
|
|
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
|
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
|
) : SearchResponse {
|
|
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
|
|
return SyncAPI.LibraryItem(
|
|
name,
|
|
url,
|
|
id,
|
|
null,
|
|
null,
|
|
null,
|
|
latestUpdatedTime,
|
|
apiName, type, posterUrl, posterHeaders, quality, this.id
|
|
)
|
|
}
|
|
}
|
|
|
|
data class ResumeWatchingResult(
|
|
@JsonProperty("name") override val name: String,
|
|
@JsonProperty("url") override val url: String,
|
|
@JsonProperty("apiName") override val apiName: String,
|
|
@JsonProperty("type") override var type: TvType? = null,
|
|
@JsonProperty("posterUrl") override var posterUrl: String?,
|
|
@JsonProperty("watchPos") val watchPos: PosDur?,
|
|
@JsonProperty("id") override var id: Int?,
|
|
@JsonProperty("parentId") val parentId: Int?,
|
|
@JsonProperty("episode") val episode: Int?,
|
|
@JsonProperty("season") val season: Int?,
|
|
@JsonProperty("isFromDownload") val isFromDownload: Boolean,
|
|
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
|
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
|
) : SearchResponse
|
|
|
|
/**
|
|
* A datastore wide account for future implementations of a multiple account system
|
|
**/
|
|
|
|
fun getAllWatchStateIds(): List<Int>? {
|
|
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
|
return getKeys(folder)?.mapNotNull {
|
|
it.removePrefix("$folder/").toIntOrNull()
|
|
}
|
|
}
|
|
|
|
fun deleteAllResumeStateIds() {
|
|
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
|
|
removeKeys(folder)
|
|
}
|
|
|
|
fun deleteAllBookmarkedData() {
|
|
val folder1 = "$currentAccount/$RESULT_WATCH_STATE"
|
|
val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA"
|
|
removeKeys(folder1)
|
|
removeKeys(folder2)
|
|
}
|
|
|
|
fun getAllResumeStateIds(): List<Int>? {
|
|
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
|
|
return getKeys(folder)?.mapNotNull {
|
|
it.removePrefix("$folder/").toIntOrNull()
|
|
}
|
|
}
|
|
|
|
private fun getAllResumeStateIdsOld(): List<Int>? {
|
|
val folder = "$currentAccount/$RESULT_RESUME_WATCHING_OLD"
|
|
return getKeys(folder)?.mapNotNull {
|
|
it.removePrefix("$folder/").toIntOrNull()
|
|
}
|
|
}
|
|
|
|
fun migrateResumeWatching() {
|
|
// if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) {
|
|
setKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, true)
|
|
getAllResumeStateIdsOld()?.forEach { id ->
|
|
getLastWatchedOld(id)?.let {
|
|
setLastWatched(
|
|
it.parentId,
|
|
null,
|
|
it.episode,
|
|
it.season,
|
|
it.isFromDownload,
|
|
it.updateTime
|
|
)
|
|
removeLastWatchedOld(it.parentId)
|
|
}
|
|
}
|
|
//}
|
|
}
|
|
|
|
fun setLastWatched(
|
|
parentId: Int?,
|
|
episodeId: Int?,
|
|
episode: Int?,
|
|
season: Int?,
|
|
isFromDownload: Boolean = false,
|
|
updateTime: Long? = null,
|
|
) {
|
|
if (parentId == null) return
|
|
setKey(
|
|
"$currentAccount/$RESULT_RESUME_WATCHING",
|
|
parentId.toString(),
|
|
VideoDownloadHelper.ResumeWatching(
|
|
parentId,
|
|
episodeId,
|
|
episode,
|
|
season,
|
|
updateTime ?: System.currentTimeMillis(),
|
|
isFromDownload
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun removeLastWatchedOld(parentId: Int?) {
|
|
if (parentId == null) return
|
|
removeKey("$currentAccount/$RESULT_RESUME_WATCHING_OLD", parentId.toString())
|
|
}
|
|
|
|
fun removeLastWatched(parentId: Int?) {
|
|
if (parentId == null) return
|
|
removeKey("$currentAccount/$RESULT_RESUME_WATCHING", parentId.toString())
|
|
}
|
|
|
|
fun getLastWatched(id: Int?): VideoDownloadHelper.ResumeWatching? {
|
|
if (id == null) return null
|
|
return getKey(
|
|
"$currentAccount/$RESULT_RESUME_WATCHING",
|
|
id.toString(),
|
|
)
|
|
}
|
|
|
|
private fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? {
|
|
if (id == null) return null
|
|
return getKey(
|
|
"$currentAccount/$RESULT_RESUME_WATCHING_OLD",
|
|
id.toString(),
|
|
)
|
|
}
|
|
|
|
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
|
|
if (id == null) return
|
|
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
|
|
AccountManager.localListApi.requireLibraryRefresh = true
|
|
}
|
|
|
|
fun getBookmarkedData(id: Int?): BookmarkedData? {
|
|
if (id == null) return null
|
|
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
|
}
|
|
|
|
fun getAllSubscriptions(): List<SubscribedData> {
|
|
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
|
|
getKey(it)
|
|
} ?: emptyList()
|
|
}
|
|
|
|
fun removeSubscribedData(id: Int?) {
|
|
if (id == null) return
|
|
AccountManager.localListApi.requireLibraryRefresh = true
|
|
removeKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
|
|
}
|
|
|
|
/**
|
|
* Set new seen episodes and update time
|
|
**/
|
|
fun updateSubscribedData(id: Int?, data: SubscribedData?, episodeResponse: EpisodeResponse?) {
|
|
if (id == null || data == null || episodeResponse == null) return
|
|
val newData = data.copy(
|
|
latestUpdatedTime = unixTimeMS,
|
|
lastSeenEpisodeCount = episodeResponse.getLatestEpisodes()
|
|
)
|
|
setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), newData)
|
|
}
|
|
|
|
fun setSubscribedData(id: Int?, data: SubscribedData) {
|
|
if (id == null) return
|
|
setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), data)
|
|
AccountManager.localListApi.requireLibraryRefresh = true
|
|
}
|
|
|
|
fun getSubscribedData(id: Int?): SubscribedData? {
|
|
if (id == null) return null
|
|
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
|
|
}
|
|
|
|
fun setViewPos(id: Int?, pos: Long, dur: Long) {
|
|
if (id == null) return
|
|
if (dur < 30_000) return // too short
|
|
setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur))
|
|
}
|
|
|
|
fun getViewPos(id: Int?): PosDur? {
|
|
if (id == null) return null
|
|
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
|
|
}
|
|
|
|
fun getVideoWatchState(id: Int?): VideoWatchState? {
|
|
if (id == null) return null
|
|
return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null)
|
|
}
|
|
|
|
fun setVideoWatchState(id: Int?, watchState: VideoWatchState) {
|
|
if (id == null) return
|
|
|
|
// None == No key
|
|
if (watchState == VideoWatchState.None) {
|
|
removeKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString())
|
|
} else {
|
|
setKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), watchState)
|
|
}
|
|
}
|
|
|
|
fun getDub(id: Int): DubStatus? {
|
|
return DubStatus.values()
|
|
.getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1)
|
|
}
|
|
|
|
fun setDub(id: Int, status: DubStatus) {
|
|
setKey("$currentAccount/$RESULT_DUB", id.toString(), status.ordinal)
|
|
}
|
|
|
|
fun setResultWatchState(id: Int?, status: Int) {
|
|
if (id == null) return
|
|
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
|
if (status == WatchType.NONE.internalId) {
|
|
removeKey(folder, id.toString())
|
|
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
|
} else {
|
|
setKey(folder, id.toString(), status)
|
|
}
|
|
}
|
|
|
|
fun getResultWatchState(id: Int): WatchType {
|
|
return WatchType.fromInternalId(
|
|
getKey<Int>(
|
|
"$currentAccount/$RESULT_WATCH_STATE",
|
|
id.toString(),
|
|
null
|
|
)
|
|
)
|
|
}
|
|
|
|
fun getResultSeason(id: Int): Int? {
|
|
return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null)
|
|
}
|
|
|
|
fun setResultSeason(id: Int, value: Int?) {
|
|
setKey("$currentAccount/$RESULT_SEASON", id.toString(), value)
|
|
}
|
|
|
|
fun getResultEpisode(id: Int): Int? {
|
|
return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null)
|
|
}
|
|
|
|
fun setResultEpisode(id: Int, value: Int?) {
|
|
setKey("$currentAccount/$RESULT_EPISODE", id.toString(), value)
|
|
}
|
|
|
|
fun addSync(id: Int, idPrefix: String, url: String) {
|
|
setKey("${idPrefix}_sync", id.toString(), url)
|
|
}
|
|
|
|
fun getSync(id: Int, idPrefixes: List<String>): List<String?> {
|
|
return idPrefixes.map { idPrefix ->
|
|
getKey("${idPrefix}_sync", id.toString())
|
|
}
|
|
}
|
|
} |