Disk cache for favicons
This commit is contained in:
@@ -42,18 +42,21 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.FaviconCache
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.PageCache
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
@@ -101,7 +104,7 @@ interface AppModule {
|
||||
fun provideCoil(
|
||||
@LocalizedAppContext context: Context,
|
||||
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
faviconFetcherFactory: FaviconFetcher.Factory,
|
||||
imageProxyInterceptor: ImageProxyInterceptor,
|
||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||
coverRestoreInterceptor: CoverRestoreInterceptor,
|
||||
@@ -138,7 +141,7 @@ interface AppModule {
|
||||
add(SvgDecoder.Factory())
|
||||
add(CbzFetcher.Factory())
|
||||
add(AvifImageDecoder.Factory())
|
||||
add(FaviconFetcher.Factory(mangaRepositoryFactory))
|
||||
add(faviconFetcherFactory)
|
||||
add(MangaPageKeyer())
|
||||
add(pageFetcherFactory)
|
||||
add(imageProxyInterceptor)
|
||||
@@ -195,5 +198,29 @@ interface AppModule {
|
||||
fun provideWorkManager(
|
||||
@ApplicationContext context: Context,
|
||||
): WorkManager = WorkManager.getInstance(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@PageCache
|
||||
fun providePageCache(
|
||||
@ApplicationContext context: Context,
|
||||
) = LocalStorageCache(
|
||||
context = context,
|
||||
dir = CacheDir.PAGES,
|
||||
defaultSize = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||
minSize = FileSize.MEGABYTES.convert(20, FileSize.BYTES),
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@FaviconCache
|
||||
fun provideFaviconCache(
|
||||
@ApplicationContext context: Context,
|
||||
) = LocalStorageCache(
|
||||
context = context,
|
||||
dir = CacheDir.FAVICONS,
|
||||
defaultSize = FileSize.MEGABYTES.convert(8, FileSize.BYTES),
|
||||
minSize = FileSize.MEGABYTES.convert(2, FileSize.BYTES),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,20 @@ import coil3.ColorImage
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.ImageFetchResult
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.size.pxOrElse
|
||||
import coil3.toAndroidUri
|
||||
import coil3.toBitmap
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.FileSystem
|
||||
import okio.IOException
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
@@ -26,8 +31,16 @@ import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.local.data.FaviconCache
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import coil3.Uri as CoilUri
|
||||
|
||||
@@ -36,6 +49,7 @@ class FaviconFetcher(
|
||||
private val options: Options,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val localStorageCache: LocalStorageCache,
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
@@ -61,6 +75,16 @@ class FaviconFetcher(
|
||||
options.size.width.pxOrElse { FALLBACK_SIZE },
|
||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||
)
|
||||
val cacheKey = options.diskCacheKey ?: "${repository.source.name}_$sizePx"
|
||||
if (options.diskCachePolicy.readEnabled) {
|
||||
localStorageCache[cacheKey]?.let { file ->
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||
mimeType = MimeTypes.probeMimeType(file)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
}
|
||||
var favicons = repository.getFavicons()
|
||||
var lastError: Exception? = null
|
||||
while (favicons.isNotEmpty()) {
|
||||
@@ -69,7 +93,11 @@ class FaviconFetcher(
|
||||
try {
|
||||
val result = imageLoader.fetch(icon.url, options)
|
||||
if (result != null) {
|
||||
return result
|
||||
return if (options.diskCachePolicy.writeEnabled) {
|
||||
writeToCache(cacheKey, result)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
} else {
|
||||
favicons -= icon
|
||||
}
|
||||
@@ -97,8 +125,39 @@ class FaviconFetcher(
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private suspend fun writeToCache(key: String, result: FetchResult): FetchResult = runCatchingCancellable {
|
||||
when (result) {
|
||||
is ImageFetchResult -> {
|
||||
if (result.dataSource == DataSource.NETWORK) {
|
||||
localStorageCache.set(key, result.image.toBitmap()).asFetchResult()
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
is SourceFetchResult -> {
|
||||
if (result.dataSource == DataSource.NETWORK) {
|
||||
result.source.source().use {
|
||||
localStorageCache.set(key, it, result.mimeType?.toMimeTypeOrNull()).asFetchResult()
|
||||
}
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(result)
|
||||
|
||||
private fun File.asFetchResult() = SourceFetchResult(
|
||||
source = ImageSource(toOkioPath(), FileSystem.SYSTEM),
|
||||
mimeType = MimeTypes.probeMimeType(this)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
|
||||
class Factory @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@FaviconCache private val faviconCache: LocalStorageCache,
|
||||
) : Fetcher.Factory<CoilUri> {
|
||||
|
||||
override fun create(
|
||||
@@ -106,7 +165,7 @@ class FaviconFetcher(
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher? = if (data.scheme == URI_SCHEME_FAVICON) {
|
||||
FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory)
|
||||
FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory, faviconCache)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||
import org.koitharu.kotatsu.local.data.PageCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
@@ -34,7 +35,7 @@ import javax.inject.Inject
|
||||
|
||||
class MangaPageFetcher(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val pagesCache: PagesCache,
|
||||
private val pagesCache: LocalStorageCache,
|
||||
private val options: Options,
|
||||
private val page: MangaPage,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@@ -53,7 +54,7 @@ class MangaPageFetcher(
|
||||
val repo = mangaRepositoryFactory.create(page.source)
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
if (options.diskCachePolicy.readEnabled) {
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
pagesCache[pageUrl]?.let { file ->
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), options.fileSystem),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
|
||||
@@ -78,7 +79,7 @@ class MangaPageFetcher(
|
||||
}
|
||||
val mimeType = response.mimeType?.toMimeTypeOrNull()
|
||||
val file = response.requireBody().use {
|
||||
pagesCache.put(pageUrl, it.source(), mimeType)
|
||||
pagesCache.set(pageUrl, it.source(), mimeType)
|
||||
}
|
||||
SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||
@@ -107,7 +108,7 @@ class MangaPageFetcher(
|
||||
|
||||
class Factory @Inject constructor(
|
||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||
private val pagesCache: PagesCache,
|
||||
@PageCache private val pagesCache: LocalStorageCache,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : Fetcher.Factory<MangaPage> {
|
||||
|
||||
@@ -76,8 +76,9 @@ import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator
|
||||
import org.koitharu.kotatsu.download.domain.DownloadProgress
|
||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.PageCache
|
||||
import org.koitharu.kotatsu.local.data.TempFileFilter
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
|
||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
||||
@@ -103,7 +104,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
@PageCache private val cache: LocalStorageCache,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val mangaLock: MangaLock,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
@@ -233,7 +234,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
semaphore.withPermit {
|
||||
runFailsafe {
|
||||
val url = repo.getPageUrl(page)
|
||||
val file = cache.get(url)
|
||||
val file = cache[url]
|
||||
?: downloadFile(url, destination, repo.source)
|
||||
output.addPage(
|
||||
chapter = chapter,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class PageCache
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class FaviconCache
|
||||
@@ -5,7 +5,6 @@ import android.graphics.Bitmap
|
||||
import android.os.StatFs
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -14,7 +13,6 @@ import okio.buffer
|
||||
import okio.sink
|
||||
import okio.use
|
||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||
@@ -28,22 +26,24 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
class LocalStorageCache(
|
||||
context: Context,
|
||||
private val dir: CacheDir,
|
||||
private val defaultSize: Long,
|
||||
private val minSize: Long,
|
||||
) {
|
||||
|
||||
private val cacheDir = suspendLazy {
|
||||
val dirs = context.externalCacheDirs + context.cacheDir
|
||||
dirs.firstNotNullOf {
|
||||
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
|
||||
it?.subdir(dir.dir)?.takeIfWriteable()
|
||||
}
|
||||
}
|
||||
private val lruCache = suspendLazy {
|
||||
val dir = cacheDir.get()
|
||||
val availableSize = (getAvailableSize() * 0.8).toLong()
|
||||
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
|
||||
val size = defaultSize.coerceAtMost(availableSize).coerceAtLeast(minSize)
|
||||
runCatchingCancellable {
|
||||
DiskLruCache.create(dir, size)
|
||||
}.recoverCatching { error ->
|
||||
@@ -54,14 +54,14 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend fun get(url: String): File? = withContext(Dispatchers.IO) {
|
||||
suspend operator fun get(url: String): File? = withContext(Dispatchers.IO) {
|
||||
val cache = lruCache.get()
|
||||
runInterruptible {
|
||||
cache.get(url)?.takeIfReadable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
|
||||
suspend operator fun set(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
|
||||
val file = createBufferFile(url, mimeType)
|
||||
try {
|
||||
val bytes = file.sink(append = false).buffer().use {
|
||||
@@ -79,7 +79,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||
suspend operator fun set(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||
val file = createBufferFile(url, MimeType("image/png"))
|
||||
try {
|
||||
bitmap.compressToPNG(file)
|
||||
@@ -107,7 +107,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(SIZE_DEFAULT)
|
||||
}.getOrDefault(defaultSize)
|
||||
|
||||
private suspend fun createBufferFile(url: String, mimeType: MimeType?): File {
|
||||
val ext = MimeTypes.getExtension(mimeType) ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
|
||||
@@ -116,13 +116,4 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
val name = UUID.randomUUID().toString() + "." + ext
|
||||
return File(rootDir, name)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val SIZE_MIN
|
||||
get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES)
|
||||
|
||||
val SIZE_DEFAULT
|
||||
get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,8 @@ import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||
import org.koitharu.kotatsu.local.data.PageCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
@@ -84,7 +85,7 @@ class PageLoader @Inject constructor(
|
||||
@LocalizedAppContext private val context: Context,
|
||||
lifecycle: ActivityRetainedLifecycle,
|
||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
@PageCache private val cache: LocalStorageCache,
|
||||
private val coil: ImageLoader,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@@ -196,7 +197,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
}
|
||||
}.use { image ->
|
||||
cache.put(uri.toString(), image).toUri()
|
||||
cache.set(uri.toString(), image).toUri()
|
||||
}
|
||||
} else {
|
||||
val file = uri.toFile()
|
||||
@@ -300,7 +301,7 @@ class PageLoader @Inject constructor(
|
||||
val request = createPageRequest(pageUrl, page.source)
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
response.requireBody().withProgress(progress).use {
|
||||
cache.put(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||
cache.set(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||
}
|
||||
}.toUri()
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class StorageManageSettingsFragment : BasePreferenceFragment(R.string.storage_us
|
||||
}
|
||||
|
||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.THUMBS)
|
||||
viewModel.clearCache(preference.key, CacheDir.THUMBS, CacheDir.FAVICONS)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -82,16 +82,18 @@ class StorageManageSettingsViewModel @Inject constructor(
|
||||
loadStorageUsage()
|
||||
}
|
||||
|
||||
fun clearCache(key: String, cache: CacheDir) {
|
||||
fun clearCache(key: String, vararg caches: CacheDir) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + key }
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
loadStorageUsage()
|
||||
if (cache == CacheDir.THUMBS || cache == CacheDir.FAVICONS) {
|
||||
coil.memoryCache?.clear()
|
||||
for (cache in caches) {
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
if (cache == CacheDir.THUMBS) {
|
||||
coil.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
loadStorageUsage()
|
||||
} finally {
|
||||
loadingKeys.update { it - key }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user