1.9.9.8 уведомление и кеширование
This commit is contained in:
@@ -2,21 +2,43 @@ package com.yaros.RadioUrl.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.yaros.RadioUrl.core.APIInterface.data.Category
|
||||
import com.yaros.RadioUrl.data.Station
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Менеджер кеширования для радиостанций и категорий
|
||||
* Менеджер кеширования для радиостанций и категорий.
|
||||
*
|
||||
* Особенности реализации:
|
||||
* - Собственный файл SharedPreferences (не дефолтный) — не разрастает
|
||||
* и не тормозит основной PreferenceManager-файл настроек на слабых устройствах.
|
||||
* - In-memory слой поверх диска — повторные обращения в рамках одной сессии
|
||||
* не парсят JSON заново.
|
||||
* - Версионирование формата кеша — при смене полей модели Station/Category
|
||||
* старый кеш не роняет приложение, а просто сбрасывается.
|
||||
* - Защита от повреждённого JSON (частая причина краша на реальных
|
||||
* устройствах при OOM/kill процесса во время записи).
|
||||
* - "Stale-if-error" — при сетевой ошибке можно отдать протухший кеш,
|
||||
* чтобы пользователь не видел пустой экран.
|
||||
* - Мягкая деградация: если init() ещё не вызван, кеш не крашит приложение,
|
||||
* а просто временно не работает (и логирует это).
|
||||
*/
|
||||
object CacheManager {
|
||||
|
||||
private const val TAG = "CacheManager"
|
||||
|
||||
// Отдельный файл, а не дефолтные PreferenceManager-настройки
|
||||
private const val PREFS_FILE_NAME = "swr_cache_manager"
|
||||
|
||||
// Версия формата кеша. Увеличивайте при изменении полей Station/Category,
|
||||
// чтобы старые несовместимые данные автоматически сбрасывались.
|
||||
private const val CACHE_FORMAT_VERSION = 1
|
||||
private const val PREF_CACHE_FORMAT_VERSION = "cache_format_version"
|
||||
|
||||
// Ключи для SharedPreferences
|
||||
private const val PREF_CACHE_ENABLED = "cache_enabled"
|
||||
private const val PREF_CACHE_MAX_SIZE = "cache_max_size_mb"
|
||||
@@ -28,7 +50,9 @@ object CacheManager {
|
||||
private const val PREF_CATEGORY_STATIONS_TIME_PREFIX = "category_stations_time_"
|
||||
|
||||
// Время жизни кеша (по умолчанию 1 час)
|
||||
private const val CACHE_EXPIRATION_TIME = 60 * 60 * 1000L // 1 час в миллисекундах
|
||||
private const val CACHE_EXPIRATION_TIME = 60 * 60 * 1000L
|
||||
// Насколько дольше можно отдавать протухший кеш как fallback при ошибке сети
|
||||
private const val STALE_FALLBACK_WINDOW = 24 * 60 * 60 * 1000L // 24 часа
|
||||
|
||||
// Размеры кеша в МБ
|
||||
const val CACHE_SIZE_SMALL = 10
|
||||
@@ -36,264 +60,288 @@ object CacheManager {
|
||||
const val CACHE_SIZE_LARGE = 50
|
||||
const val CACHE_SIZE_XLARGE = 100
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
@Volatile
|
||||
private var sharedPreferences: SharedPreferences? = null
|
||||
private val gson = Gson()
|
||||
|
||||
// Простой in-memory слой: избавляет от повторного чтения/парсинга JSON
|
||||
// на слабых устройствах в рамках одной "живой" сессии процесса.
|
||||
private data class MemoryEntry(val json: String, val timestamp: Long)
|
||||
private val memoryCache = ConcurrentHashMap<String, MemoryEntry>()
|
||||
|
||||
fun init(context: Context) {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (sharedPreferences == null) {
|
||||
synchronized(this) {
|
||||
if (sharedPreferences == null) {
|
||||
val prefs = context.applicationContext
|
||||
.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
|
||||
sharedPreferences = prefs
|
||||
migrateFormatIfNeeded(prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, включено ли кеширование
|
||||
* Если версия формата кеша изменилась (например, обновили модель Station
|
||||
* новым полем без дефолта, и старый Gson.fromJson упадёт) — сбрасываем кеш
|
||||
* целиком одним махом вместо падения на первом же чтении.
|
||||
*/
|
||||
private fun migrateFormatIfNeeded(prefs: SharedPreferences) {
|
||||
val storedVersion = prefs.getInt(PREF_CACHE_FORMAT_VERSION, -1)
|
||||
if (storedVersion != CACHE_FORMAT_VERSION) {
|
||||
Timber.tag(TAG).i("Cache format changed ($storedVersion -> $CACHE_FORMAT_VERSION), clearing cache")
|
||||
prefs.edit()
|
||||
.clear()
|
||||
.putInt(PREF_CACHE_FORMAT_VERSION, CACHE_FORMAT_VERSION)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefs(): SharedPreferences? {
|
||||
val p = sharedPreferences
|
||||
if (p == null) {
|
||||
Timber.tag(TAG).w("CacheManager used before init() — caching disabled for this call")
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
/** Проверка, включено ли кеширование */
|
||||
fun isCachingEnabled(): Boolean {
|
||||
return sharedPreferences.getBoolean(PREF_CACHE_ENABLED, true)
|
||||
return prefs()?.getBoolean(PREF_CACHE_ENABLED, true) ?: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Включить кеширование
|
||||
*/
|
||||
fun enableCaching() {
|
||||
sharedPreferences.edit().putBoolean(PREF_CACHE_ENABLED, true).apply()
|
||||
prefs()?.edit()?.putBoolean(PREF_CACHE_ENABLED, true)?.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключить кеширование
|
||||
*/
|
||||
fun disableCaching() {
|
||||
sharedPreferences.edit().putBoolean(PREF_CACHE_ENABLED, false).apply()
|
||||
prefs()?.edit()?.putBoolean(PREF_CACHE_ENABLED, false)?.apply()
|
||||
clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить максимальный размер кеша в МБ
|
||||
*/
|
||||
fun getMaxCacheSize(): Int {
|
||||
return sharedPreferences.getInt(PREF_CACHE_MAX_SIZE, CACHE_SIZE_MEDIUM)
|
||||
return prefs()?.getInt(PREF_CACHE_MAX_SIZE, CACHE_SIZE_MEDIUM) ?: CACHE_SIZE_MEDIUM
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить максимальный размер кеша в МБ
|
||||
*/
|
||||
fun setMaxCacheSize(sizeMb: Int) {
|
||||
sharedPreferences.edit().putInt(PREF_CACHE_MAX_SIZE, sizeMb).apply()
|
||||
prefs()?.edit()?.putInt(PREF_CACHE_MAX_SIZE, sizeMb)?.apply()
|
||||
enforceMaxCacheSize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить список всех радиостанций в кеш
|
||||
*/
|
||||
// ---------------------------------------------------------------------
|
||||
// Station list (все станции)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
fun cacheStations(stations: List<Station>) {
|
||||
if (!isCachingEnabled()) return
|
||||
|
||||
try {
|
||||
val json = gson.toJson(stations)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putString(PREF_STATIONS_CACHE, json)
|
||||
.putLong(PREF_STATIONS_CACHE_TIME, currentTime)
|
||||
.apply()
|
||||
|
||||
Timber.tag(TAG).d("Cached ${stations.size} stations")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error caching stations")
|
||||
}
|
||||
writeEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, stations, "stations")
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список всех радиостанций из кеша
|
||||
*/
|
||||
fun getCachedStations(): List<Station>? {
|
||||
if (!isCachingEnabled()) return null
|
||||
|
||||
try {
|
||||
val cacheTime = sharedPreferences.getLong(PREF_STATIONS_CACHE_TIME, 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// Проверяем, не истек ли срок действия кеша
|
||||
if (currentTime - cacheTime > CACHE_EXPIRATION_TIME) {
|
||||
Timber.tag(TAG).d("Stations cache expired")
|
||||
return null
|
||||
}
|
||||
|
||||
val json = sharedPreferences.getString(PREF_STATIONS_CACHE, null) ?: return null
|
||||
val type = object : TypeToken<List<Station>>() {}.type
|
||||
val stations: List<Station> = gson.fromJson(json, type)
|
||||
|
||||
Timber.tag(TAG).d("Retrieved ${stations.size} stations from cache")
|
||||
return stations
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error retrieving cached stations")
|
||||
return null
|
||||
}
|
||||
return readEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, allowStale = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить список категорий в кеш
|
||||
*/
|
||||
/** То же самое, но если кеш протух — вернёт его всё равно (для fallback при ошибке сети). */
|
||||
fun getCachedStationsOrStale(): List<Station>? {
|
||||
return readEntry(PREF_STATIONS_CACHE, PREF_STATIONS_CACHE_TIME, allowStale = true)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Categories
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
fun cacheCategories(categories: List<Category>) {
|
||||
if (!isCachingEnabled()) return
|
||||
|
||||
try {
|
||||
val json = gson.toJson(categories)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putString(PREF_CATEGORIES_CACHE, json)
|
||||
.putLong(PREF_CATEGORIES_CACHE_TIME, currentTime)
|
||||
.apply()
|
||||
|
||||
Timber.tag(TAG).d("Cached ${categories.size} categories")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error caching categories")
|
||||
}
|
||||
writeEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, categories, "categories")
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список категорий из кеша
|
||||
*/
|
||||
fun getCachedCategories(): List<Category>? {
|
||||
if (!isCachingEnabled()) return null
|
||||
|
||||
try {
|
||||
val cacheTime = sharedPreferences.getLong(PREF_CATEGORIES_CACHE_TIME, 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// Проверяем, не истек ли срок действия кеша
|
||||
if (currentTime - cacheTime > CACHE_EXPIRATION_TIME) {
|
||||
Timber.tag(TAG).d("Categories cache expired")
|
||||
return null
|
||||
}
|
||||
|
||||
val json = sharedPreferences.getString(PREF_CATEGORIES_CACHE, null) ?: return null
|
||||
val type = object : TypeToken<List<Category>>() {}.type
|
||||
val categories: List<Category> = gson.fromJson(json, type)
|
||||
|
||||
Timber.tag(TAG).d("Retrieved ${categories.size} categories from cache")
|
||||
return categories
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error retrieving cached categories")
|
||||
return null
|
||||
}
|
||||
return readEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, allowStale = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить список радиостанций категории в кеш
|
||||
*/
|
||||
fun getCachedCategoriesOrStale(): List<Category>? {
|
||||
return readEntry(PREF_CATEGORIES_CACHE, PREF_CATEGORIES_CACHE_TIME, allowStale = true)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Stations by category
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
fun cacheCategoryStations(categoryId: Int, stations: List<Station>) {
|
||||
writeEntry(
|
||||
"$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
|
||||
"$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
|
||||
stations,
|
||||
"category $categoryId stations"
|
||||
)
|
||||
}
|
||||
|
||||
fun getCachedCategoryStations(categoryId: Int): List<Station>? {
|
||||
return readEntry(
|
||||
"$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
|
||||
"$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
|
||||
allowStale = false
|
||||
)
|
||||
}
|
||||
|
||||
fun getCachedCategoryStationsOrStale(categoryId: Int): List<Station>? {
|
||||
return readEntry(
|
||||
"$PREF_CATEGORY_STATIONS_PREFIX$categoryId",
|
||||
"$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId",
|
||||
allowStale = true
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Общая реализация чтения/записи
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private inline fun <reified T> writeEntry(
|
||||
dataKey: String,
|
||||
timeKey: String,
|
||||
data: List<T>,
|
||||
logLabel: String
|
||||
) {
|
||||
if (!isCachingEnabled()) return
|
||||
|
||||
val p = prefs() ?: return
|
||||
try {
|
||||
val json = gson.toJson(stations)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putString("$PREF_CATEGORY_STATIONS_PREFIX$categoryId", json)
|
||||
.putLong("$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId", currentTime)
|
||||
val json = gson.toJson(data)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
p.edit()
|
||||
.putString(dataKey, json)
|
||||
.putLong(timeKey, timestamp)
|
||||
.apply()
|
||||
|
||||
Timber.tag(TAG).d("Cached ${stations.size} stations for category $categoryId")
|
||||
memoryCache[dataKey] = MemoryEntry(json, timestamp)
|
||||
Timber.tag(TAG).d("Cached ${data.size} $logLabel")
|
||||
enforceMaxCacheSize()
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error caching category stations")
|
||||
Timber.tag(TAG).e(e, "Error caching $logLabel")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список радиостанций категории из кеша
|
||||
*/
|
||||
fun getCachedCategoryStations(categoryId: Int): List<Station>? {
|
||||
private inline fun <reified T> readEntry(
|
||||
dataKey: String,
|
||||
timeKey: String,
|
||||
allowStale: Boolean
|
||||
): List<T>? {
|
||||
if (!isCachingEnabled()) return null
|
||||
val p = prefs() ?: return null
|
||||
|
||||
try {
|
||||
val cacheTime = sharedPreferences.getLong("$PREF_CATEGORY_STATIONS_TIME_PREFIX$categoryId", 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// Проверяем, не истек ли срок действия кеша
|
||||
if (currentTime - cacheTime > CACHE_EXPIRATION_TIME) {
|
||||
Timber.tag(TAG).d("Category $categoryId stations cache expired")
|
||||
// Сначала пробуем in-memory слой — не трогаем диск лишний раз
|
||||
memoryCache[dataKey]?.let { mem ->
|
||||
val age = now - mem.timestamp
|
||||
if (age <= CACHE_EXPIRATION_TIME || (allowStale && age <= STALE_FALLBACK_WINDOW)) {
|
||||
return parseOrEvict(dataKey, mem.json)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
val cacheTime = p.getLong(timeKey, 0)
|
||||
val age = now - cacheTime
|
||||
val withinFreshWindow = age <= CACHE_EXPIRATION_TIME
|
||||
val withinStaleWindow = allowStale && age <= STALE_FALLBACK_WINDOW
|
||||
|
||||
if (!withinFreshWindow && !withinStaleWindow) {
|
||||
Timber.tag(TAG).d("Cache for $dataKey expired (age=${age}ms)")
|
||||
return null
|
||||
}
|
||||
|
||||
val json = sharedPreferences.getString("$PREF_CATEGORY_STATIONS_PREFIX$categoryId", null) ?: return null
|
||||
val type = object : TypeToken<List<Station>>() {}.type
|
||||
val stations: List<Station> = gson.fromJson(json, type)
|
||||
|
||||
Timber.tag(TAG).d("Retrieved ${stations.size} stations for category $categoryId from cache")
|
||||
return stations
|
||||
val json = p.getString(dataKey, null) ?: return null
|
||||
memoryCache[dataKey] = MemoryEntry(json, cacheTime)
|
||||
parseOrEvict(dataKey, json)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error retrieving cached category stations")
|
||||
return null
|
||||
Timber.tag(TAG).e(e, "Error retrieving cache for $dataKey")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить весь кеш
|
||||
*/
|
||||
fun clearCache() {
|
||||
try {
|
||||
val editor = sharedPreferences.edit()
|
||||
private inline fun <reified T> parseOrEvict(dataKey: String, json: String): List<T>? {
|
||||
return try {
|
||||
val type = TypeToken.getParameterized(List::class.java, T::class.java).type
|
||||
gson.fromJson<List<T>>(json, type)
|
||||
} catch (e: JsonParseException) {
|
||||
// Повреждённый или несовместимый JSON — не роняем вызывающий код,
|
||||
// а тихо чистим битую запись, чтобы она не мешала при следующем чтении.
|
||||
Timber.tag(TAG).e(e, "Corrupted cache entry for $dataKey, evicting")
|
||||
evictEntry(dataKey)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем кеш станций
|
||||
private fun evictEntry(dataKey: String) {
|
||||
memoryCache.remove(dataKey)
|
||||
prefs()?.edit()?.remove(dataKey)?.apply()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Обслуживание кеша
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/** Очистить весь кеш (кроме флагов enabled/max size/версии формата) */
|
||||
fun clearCache() {
|
||||
val p = prefs() ?: return
|
||||
try {
|
||||
val editor = p.edit()
|
||||
editor.remove(PREF_STATIONS_CACHE)
|
||||
editor.remove(PREF_STATIONS_CACHE_TIME)
|
||||
|
||||
// Удаляем кеш категорий
|
||||
editor.remove(PREF_CATEGORIES_CACHE)
|
||||
editor.remove(PREF_CATEGORIES_CACHE_TIME)
|
||||
|
||||
// Удаляем кеш станций по категориям
|
||||
val allKeys = sharedPreferences.all.keys
|
||||
allKeys.forEach { key ->
|
||||
p.all.keys.forEach { key ->
|
||||
if (key.startsWith(PREF_CATEGORY_STATIONS_PREFIX) ||
|
||||
key.startsWith(PREF_CATEGORY_STATIONS_TIME_PREFIX)) {
|
||||
key.startsWith(PREF_CATEGORY_STATIONS_TIME_PREFIX)
|
||||
) {
|
||||
editor.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
editor.apply()
|
||||
memoryCache.clear()
|
||||
Timber.tag(TAG).d("Cache cleared")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error clearing cache")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить размер кеша в байтах (приблизительно)
|
||||
*/
|
||||
/** Приблизительный размер кеша в байтах */
|
||||
fun getCacheSize(): Long {
|
||||
try {
|
||||
val p = prefs() ?: return 0L
|
||||
return try {
|
||||
var totalSize = 0L
|
||||
|
||||
// Размер кеша станций
|
||||
sharedPreferences.getString(PREF_STATIONS_CACHE, null)?.let {
|
||||
totalSize += it.length * 2 // UTF-16, 2 байта на символ
|
||||
}
|
||||
|
||||
// Размер кеша категорий
|
||||
sharedPreferences.getString(PREF_CATEGORIES_CACHE, null)?.let {
|
||||
totalSize += it.length * 2
|
||||
}
|
||||
|
||||
// Размер кеша станций по категориям
|
||||
val allKeys = sharedPreferences.all.keys
|
||||
allKeys.forEach { key ->
|
||||
p.getString(PREF_STATIONS_CACHE, null)?.let { totalSize += it.length * 2L }
|
||||
p.getString(PREF_CATEGORIES_CACHE, null)?.let { totalSize += it.length * 2L }
|
||||
p.all.keys.forEach { key ->
|
||||
if (key.startsWith(PREF_CATEGORY_STATIONS_PREFIX)) {
|
||||
sharedPreferences.getString(key, null)?.let {
|
||||
totalSize += it.length * 2
|
||||
}
|
||||
p.getString(key, null)?.let { totalSize += it.length * 2L }
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize
|
||||
totalSize
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error calculating cache size")
|
||||
return 0L
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun getCacheSizeMB(): Double = getCacheSize() / (1024.0 * 1024.0)
|
||||
|
||||
/**
|
||||
* Получить размер кеша в МБ
|
||||
* Если кеш превысил заданный пользователем лимит (например, на устройствах
|
||||
* с малым объёмом памяти/хранилища) — сбрасываем самые "тяжёлые" данные
|
||||
* по категориям (они менее критичны, чем общий список станций).
|
||||
*/
|
||||
fun getCacheSizeMB(): Double {
|
||||
return getCacheSize() / (1024.0 * 1024.0)
|
||||
private fun enforceMaxCacheSize() {
|
||||
val maxBytes = getMaxCacheSize() * 1024L * 1024L
|
||||
if (getCacheSize() <= maxBytes) return
|
||||
|
||||
val p = prefs() ?: return
|
||||
Timber.tag(TAG).w("Cache exceeds max size, trimming per-category entries")
|
||||
val categoryKeys = p.all.keys.filter { it.startsWith(PREF_CATEGORY_STATIONS_PREFIX) }
|
||||
val editor = p.edit()
|
||||
categoryKeys.forEach { key ->
|
||||
editor.remove(key)
|
||||
editor.remove(key.replace(PREF_CATEGORY_STATIONS_PREFIX, PREF_CATEGORY_STATIONS_TIME_PREFIX))
|
||||
memoryCache.remove(key)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user