Files
Pupil/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
2021-07-12 08:44:43 +09:00

136 lines
4.9 KiB
Kotlin

/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.pupil.Pupil
import java.io.File
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class ImageCache(context: Context) : DIAware {
override val di by closestDI(context)
private val applicationContext: Pupil by instance()
private val client: HttpClient by instance()
val cacheFolder = File(context.cacheDir, "imageCache")
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
val channels = _channels as Map<String, Channel<Float>>
private val requests = mutableMapOf<String, Job>()
@Synchronized
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun cleanup() = coroutineScope {
val LIMIT = 100*1024*1024
cacheFolder.listFiles { it -> it.canonicalPath !in cache.values || it.name == ".cache" }?.forEach { it.delete() }
if (cacheFolder.size() > LIMIT)
do {
cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
File(it.value).delete()
cache.remove(it.key)
}
} while (cacheFolder.size() > LIMIT / 2)
}
fun free(images: List<String>) {
images.forEach {
requests[it]?.cancel()
}
images.forEach { _channels.remove(it) }
}
@Synchronized
suspend fun clear() = coroutineScope {
requests.values.forEach { it.cancel() }
cacheFolder.listFiles()?.forEach { it.delete() }
cache.clear()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File {
val request = HttpRequestBuilder().apply(requestBuilder)
val key = request.url.buildString()
val progressChannel = if (_channels[key]?.isClosedForSend == false)
_channels[key]!!
else
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
return cache[key]?.let {
progressChannel.close()
File(it)
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
if (!file.exists())
file.createNewFile()
cache[key] = file.canonicalPath
requests[key] = CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
client.get<HttpStatement>(request).execute { httpResponse ->
val responseChannel: ByteReadChannel = httpResponse.receive()
val contentLength = httpResponse.contentLength() ?: -1
var readBytes = 0F
while (!responseChannel.isClosedForRead) {
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
file.appendBytes(bytes)
readBytes += bytes.size
progressChannel.trySend(readBytes / contentLength)
}
}
progressChannel.close()
}
}.onFailure {
file.delete()
cache.remove(key)
FirebaseCrashlytics.getInstance().recordException(it)
progressChannel.close(it)
}
}
}
}
}