Refine EhTagDatabase & Cleanup java io usage
* Further kill some java io usage * Further optimize TagDatabase update logic * Further optimize TagDatabase * Move some part from okio to kxio * Address Code Review * Fix to not reinit with current if inited * Convert readLegacySpiderInfo to use kxio * Refine * Refine * Refine --------- Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
1 parent
7bd36dd274
commit
8c766115db
20 files changed
+169
-238
No files matched your search
@@ -70,6 +70,7 @@ import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.ehviewer.util.isAtLeastSExtension7
|
||||
import com.hippo.files.deleteContent
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -112,7 +113,7 @@ class EhApplication :
|
||||
System.loadLibrary("ehviewer")
|
||||
lifecycleScope.launchIO {
|
||||
launchUI { FavouriteStatusRouter.collect { (gid, slot) -> detailCache[gid]?.favoriteSlot = slot } }
|
||||
launch { EhTagDatabase }
|
||||
EhTagDatabase.launchUpdate()
|
||||
launch { EhDB }
|
||||
launchIO { dataStateFlow.value }
|
||||
launchIO { OSUtils.totalMemory }
|
||||
@@ -150,14 +151,8 @@ class EhApplication :
|
||||
}
|
||||
|
||||
private fun clearTempDir() {
|
||||
var dir = AppConfig.tempDir
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir)
|
||||
}
|
||||
dir = AppConfig.externalTempDir
|
||||
if (null != dir) {
|
||||
FileUtils.deleteContent(dir)
|
||||
}
|
||||
AppConfig.tempDir.deleteContent()
|
||||
AppConfig.externalTempDir?.deleteContent()
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
|
||||
@@ -65,11 +65,5 @@ fun updateWhenGallerySiteChanges() {
|
||||
}
|
||||
|
||||
fun updateWhenTagTranslationChanges() {
|
||||
collectScope.launchIO {
|
||||
runCatching {
|
||||
EhTagDatabase.update()
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
EhTagDatabase.launchUpdate()
|
||||
}
|
||||
@@ -32,13 +32,13 @@ import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.ParametersBuilder
|
||||
import io.ktor.http.content.TextContent
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.io.Source
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArrayBuilder
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import okio.BufferedSource
|
||||
import kotlinx.serialization.json.io.decodeFromSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
@@ -47,7 +47,7 @@ suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely
|
||||
it.bodyAsUtf8Text().parseAs<T>()
|
||||
}
|
||||
|
||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||
inline fun <reified T> Source.parseAs(): T = json.decodeFromSource(this)
|
||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||
|
||||
val json = Json {
|
||||
|
||||
@@ -24,29 +24,30 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.TagNamespace
|
||||
import com.hippo.ehviewer.ui.screen.implicit
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import com.hippo.ehviewer.util.ensureSuccess
|
||||
import com.hippo.ehviewer.util.sha1
|
||||
import com.hippo.ehviewer.util.utf8
|
||||
import com.hippo.files.delete
|
||||
import com.hippo.files.moveTo
|
||||
import com.hippo.files.read
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.io.Source
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.BufferedSource
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import okio.Path
|
||||
import splitties.init.appCtx
|
||||
|
||||
object EhTagDatabase : CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private const val NAMESPACE_PREFIX = "n"
|
||||
private val PREFIXES = arrayOf(
|
||||
"-",
|
||||
@@ -56,8 +57,6 @@ object EhTagDatabase : CoroutineScope {
|
||||
)
|
||||
private lateinit var tagGroups: Map<String, Map<String, String>>
|
||||
private val updateLock = Mutex()
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
|
||||
var initialized by mutableStateOf(false)
|
||||
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||
@@ -101,18 +100,12 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
private fun metadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
|
||||
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
|
||||
|
||||
private fun getFileContent(file: File): String? = runCatching {
|
||||
file.source().buffer().use { it.readUtf8() }
|
||||
}.getOrNull()
|
||||
|
||||
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == data.toOkioPath().sha1()
|
||||
|
||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||
private suspend fun fetch(client: HttpClient, url: String, file: Path) {
|
||||
runCatching {
|
||||
client.prepareGet(url).executeSafely {
|
||||
it.status.ensureSuccess()
|
||||
@@ -123,89 +116,70 @@ object EhTagDatabase : CoroutineScope {
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend fun update() {
|
||||
fun launchUpdate() = launch {
|
||||
updateLock.withLock {
|
||||
updateInternal()
|
||||
}
|
||||
}
|
||||
|
||||
private fun issueUpdateInMemoryData(file: File? = null) {
|
||||
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
|
||||
val dataName = metadata[2]
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
File(dir, dataName).takeIf { it.exists() }
|
||||
} ?: return
|
||||
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
updateLock.withLock {
|
||||
runSuspendCatching {
|
||||
issueUpdateInMemoryData()
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
updateInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateInternal() {
|
||||
getMetadata(appCtx)?.let { metadata ->
|
||||
val sha1Name = metadata[0]
|
||||
val sha1Url = metadata[1]
|
||||
val dataName = metadata[2]
|
||||
val dataUrl = metadata[3]
|
||||
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
checkNotNull(dir)
|
||||
val sha1File = File(dir, sha1Name)
|
||||
val dataFile = File(dir, dataName)
|
||||
|
||||
runSuspendCatching {
|
||||
// Check current sha1 and current data
|
||||
val sha1 = getFileContent(sha1File)
|
||||
if (!checkData(sha1, dataFile)) {
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
}
|
||||
|
||||
// Save new sha1
|
||||
val tempSha1File = File(dir, "$sha1Name.tmp")
|
||||
save(ktorClient, sha1Url, tempSha1File)
|
||||
val tempSha1 = getFileContent(tempSha1File)
|
||||
|
||||
// Check new sha1 and current sha1
|
||||
if (tempSha1 == sha1) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File)
|
||||
return
|
||||
}
|
||||
|
||||
// Save new data
|
||||
val tempDataFile = File(dir, "$dataName.tmp")
|
||||
save(ktorClient, dataUrl, tempDataFile)
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File)
|
||||
FileUtils.delete(tempDataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
tempSha1File.renameTo(sha1File)
|
||||
tempDataFile.renameTo(dataFile)
|
||||
|
||||
// Read new EhTagDatabase
|
||||
issueUpdateInMemoryData(dataFile)
|
||||
atomicallyUpdate()
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun atomicallyUpdate() {
|
||||
val (sha1Name, sha1Url, dataName, dataUrl) = metadata(appCtx) ?: return
|
||||
val workdir = AppConfig.tagTranslationsDir
|
||||
val sha1File = workdir / sha1Name
|
||||
val dataFile = workdir / dataName
|
||||
|
||||
fun updateInMemoryData() {
|
||||
tagGroups = dataFile.read(Source::parseAs)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
// Check current sha1 and current data
|
||||
val currentSha1 = runSuspendCatching {
|
||||
dataFile.sha1().also {
|
||||
check(sha1File.utf8() == it)
|
||||
if (!initialized) updateInMemoryData()
|
||||
}
|
||||
}.onFailure {
|
||||
sha1File.delete()
|
||||
dataFile.delete()
|
||||
}.getOrNull()
|
||||
|
||||
// Save new sha1
|
||||
val tempSha1File = workdir / "$sha1Name.tmp"
|
||||
fetch(ktorClient, sha1Url, tempSha1File)
|
||||
val tempSha1 = tempSha1File.utf8()
|
||||
|
||||
// Check new sha1 and current sha1
|
||||
if (tempSha1 == currentSha1) {
|
||||
// The data is the same
|
||||
tempSha1File.delete()
|
||||
return
|
||||
}
|
||||
|
||||
// Save new data
|
||||
val tempDataFile = workdir / "$dataName.tmp"
|
||||
fetch(ktorClient, dataUrl, tempDataFile)
|
||||
|
||||
// Check new sha1 and new data
|
||||
runSuspendCatching {
|
||||
check(tempDataFile.sha1() == tempSha1)
|
||||
}.onFailure {
|
||||
tempSha1File.delete()
|
||||
tempDataFile.delete()
|
||||
throw it
|
||||
}.onSuccess {
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
sha1File.delete()
|
||||
dataFile.delete()
|
||||
tempSha1File moveTo sha1File
|
||||
tempDataFile moveTo dataFile
|
||||
|
||||
// Read new EhTagDatabase
|
||||
updateInMemoryData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,4 +830,4 @@ var downloadLocation: Path
|
||||
|
||||
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
||||
val DownloadInfo.archiveFile get() = downloadDir?.run { find("$gid.cbz") ?: find("$gid.zip") }
|
||||
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")?.toOkioPath()
|
||||
val GalleryInfo.tempDownloadDir get() = AppConfig.externalTempPersistDir?.let { it / "$gid" }
|
||||
@@ -1,17 +1,15 @@
|
||||
package com.hippo.ehviewer.legacy
|
||||
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import java.io.InputStream
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import com.hippo.files.read
|
||||
import kotlinx.io.readLineStrict
|
||||
import okio.Path
|
||||
|
||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
val source = inputStream.source().buffer()
|
||||
fun read(): String = source.readUtf8LineStrict()
|
||||
fun Path.readLegacySpiderInfo() = read {
|
||||
fun read() = readLineStrict()
|
||||
fun readInt() = read().toInt()
|
||||
fun readLong() = read().toLong()
|
||||
|
||||
fun readInt(): Int = read().toInt()
|
||||
|
||||
fun readLong(): Long = read().toLong()
|
||||
repeat(2) { read() } // We assert that only info v2
|
||||
val gid = readLong()
|
||||
val token = read()
|
||||
@@ -20,19 +18,19 @@ fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
val previewPerPage = readInt()
|
||||
val pages = read().toInt()
|
||||
val pTokenMap = hashMapOf<Int, String>()
|
||||
val info = SpiderInfo(gid, token, pages, pTokenMap, previewPages, previewPerPage)
|
||||
runCatching {
|
||||
while (true) {
|
||||
val line = read()
|
||||
val pos = line.indexOf(" ")
|
||||
if (pos > 0) {
|
||||
val index = line.substring(0, pos).toInt()
|
||||
val pToken = line.substring(pos + 1)
|
||||
if (pToken.isNotEmpty()) {
|
||||
pTokenMap[index] = pToken
|
||||
SpiderInfo(gid, token, pages, pTokenMap, previewPages, previewPerPage).apply {
|
||||
runCatching {
|
||||
while (true) {
|
||||
val line = read()
|
||||
val pos = line.indexOf(" ")
|
||||
if (pos > 0) {
|
||||
val index = line.substring(0, pos).toInt()
|
||||
val pToken = line.substring(pos + 1)
|
||||
if (pToken.isNotEmpty()) {
|
||||
pTokenMap[index] = pToken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -16,8 +16,8 @@ import com.hippo.ehviewer.client.data.TagNamespace.Male
|
||||
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
|
||||
import com.hippo.ehviewer.client.data.TagNamespace.Other
|
||||
import com.hippo.ehviewer.client.data.TagNamespace.Parody
|
||||
import com.hippo.files.openInputStream
|
||||
import com.hippo.files.openOutputStream
|
||||
import com.hippo.files.inputStream
|
||||
import com.hippo.files.outputStream
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -107,7 +107,7 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
|
||||
).flatten().ifEmpty { null }
|
||||
|
||||
fun ComicInfo.write(file: Path) {
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
file.outputStream().bufferedWriter().use {
|
||||
xmlStreaming.newWriter(it).use { writer ->
|
||||
xml.encodeToWriter(writer, ComicInfo.serializer(), this)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ fun ComicInfo.write(file: Path) {
|
||||
}
|
||||
|
||||
fun readComicInfo(file: Path): ComicInfo? = runCatching {
|
||||
file.openInputStream().bufferedReader().use {
|
||||
file.inputStream().bufferedReader().use {
|
||||
xmlStreaming.newReader(it).use { reader ->
|
||||
xml.decodeFromReader(ComicInfo.serializer(), reader)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import com.hippo.files.list
|
||||
import com.hippo.files.mkdirs
|
||||
import com.hippo.files.moveTo
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.openOutputStream
|
||||
import com.hippo.files.outputStream
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.plugins.timeout
|
||||
@@ -198,7 +198,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
val file = resolve(tempFile.name.removeSuffix(TEMP_SUFFIX))
|
||||
file.delete()
|
||||
lock.write { fileCache.remove(file.name) }
|
||||
tempFile.moveTo(file)
|
||||
tempFile moveTo file
|
||||
lock.write {
|
||||
fileCache.remove(tempFile.name)
|
||||
fileCache[file.name] = file
|
||||
@@ -221,7 +221,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
val url = response.request.url.toString()
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url).ifEmpty { "jpg" }
|
||||
return saveResponseMeta(index, extension) { outFile ->
|
||||
outFile.openOutputStream().use {
|
||||
outFile.outputStream().use {
|
||||
response.bodyAsChannel().copyTo(it.channel)
|
||||
}
|
||||
FileHashRegex.find(url)?.let {
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.hippo.ehviewer.coil.edit
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
|
||||
import com.hippo.files.openInputStream
|
||||
import com.hippo.files.openOutputStream
|
||||
import com.hippo.files.outputStream
|
||||
import com.hippo.files.read
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.io.readByteArray
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
@@ -36,7 +37,7 @@ private val cbor = Cbor {
|
||||
}
|
||||
|
||||
fun SpiderInfo.write(file: Path) {
|
||||
file.openOutputStream().use {
|
||||
file.outputStream().use {
|
||||
it.write(cbor.encodeToByteArray(this))
|
||||
}
|
||||
}
|
||||
@@ -60,18 +61,14 @@ private val spiderInfoCache by lazy {
|
||||
|
||||
fun readFromCache(gid: Long): SpiderInfo? = spiderInfoCache.read(gid.toString()) {
|
||||
runCatching {
|
||||
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
|
||||
data.read { cbor.decodeFromByteArray<SpiderInfo>(readByteArray()) }
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun readCompatFromPath(file: Path): SpiderInfo? = runCatching {
|
||||
file.openInputStream().use {
|
||||
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
||||
}
|
||||
file.read { cbor.decodeFromByteArray<SpiderInfo>(readByteArray()) }
|
||||
}.getOrNull() ?: runCatching {
|
||||
file.openInputStream().use { readLegacySpiderInfo(it) }
|
||||
file.readLegacySpiderInfo()
|
||||
}.getOrNull()
|
||||
|
||||
const val TOKEN_FAILED = "failed"
|
||||
@@ -68,7 +68,7 @@ import com.hippo.ehviewer.util.toEpochMillis
|
||||
import com.hippo.ehviewer.util.toLocalDateTime
|
||||
import com.hippo.files.delete
|
||||
import com.hippo.files.isDirectory
|
||||
import com.hippo.files.openOutputStream
|
||||
import com.hippo.files.outputStream
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -89,7 +89,7 @@ private fun removeNoMediaFile(downloadDir: Path) {
|
||||
}
|
||||
|
||||
private fun ensureNoMediaFile(downloadDir: Path) {
|
||||
(downloadDir / ".nomedia").openOutputStream().close()
|
||||
(downloadDir / ".nomedia").outputStream().close()
|
||||
}
|
||||
|
||||
private val lck = Mutex()
|
||||
|
||||
@@ -37,8 +37,8 @@ context(PageLoader)
|
||||
private fun Context.provideImage(index: Int): Uri? {
|
||||
val dir = AppConfig.externalTempDir ?: return null
|
||||
val name = getImageFilename(index) ?: return null
|
||||
val file = File(dir, name).takeIf { save(index, it.toOkioPath()) } ?: return null
|
||||
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file)
|
||||
val file = (dir / name).takeIf { save(index, it) } ?: return null
|
||||
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file.toFile())
|
||||
}
|
||||
|
||||
context(SnackbarHostState, Context, PageLoader)
|
||||
|
||||
@@ -44,11 +44,11 @@ import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.installPackage
|
||||
import com.hippo.files.delete
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
@@ -157,7 +157,7 @@ suspend fun DialogState.showNewVersion(context: Context, release: Release) {
|
||||
EhDB.exportDB(context, (downloadLocation / "$time.db"))
|
||||
}
|
||||
// TODO: Download in the background and show progress in notification
|
||||
val file = File(AppConfig.tempDir, "update.apk").apply { delete() }
|
||||
AppUpdater.downloadUpdate(release.downloadLink, file)
|
||||
withUIContext { context.installPackage(file) }
|
||||
val path = AppConfig.tempDir / "update.apk"
|
||||
AppUpdater.downloadUpdate(release.downloadLink, path.apply { delete() })
|
||||
withUIContext { context.installPackage(path.toFile()) }
|
||||
}
|
||||
@@ -7,18 +7,19 @@ import com.hippo.ehviewer.client.executeAndParseAs
|
||||
import com.hippo.ehviewer.client.executeSafely
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import com.hippo.ehviewer.util.ensureSuccess
|
||||
import com.hippo.files.outputStream
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import java.io.File
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.Path
|
||||
import tachiyomi.data.release.GithubArtifacts
|
||||
import tachiyomi.data.release.GithubCommitComparison
|
||||
import tachiyomi.data.release.GithubRelease
|
||||
@@ -68,7 +69,7 @@ object AppUpdater {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun downloadUpdate(url: String, file: File) = ghStatement(url) {
|
||||
suspend fun downloadUpdate(url: String, path: Path) = ghStatement(url) {
|
||||
timeout {
|
||||
requestTimeoutMillis = 60_000
|
||||
}
|
||||
@@ -78,13 +79,13 @@ object AppUpdater {
|
||||
response.bodyAsChannel().toInputStream().use { stream ->
|
||||
ZipInputStream(stream).use { zip ->
|
||||
zip.nextEntry
|
||||
file.outputStream().use {
|
||||
path.outputStream().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.bodyAsChannel().copyTo(file)
|
||||
response.bodyAsChannel().copyTo(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,25 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.client.parser.ParserUtils
|
||||
import com.hippo.files.exists
|
||||
import com.hippo.files.isDirectory
|
||||
import com.hippo.files.mkdirs
|
||||
import java.io.File
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.init.appCtx
|
||||
|
||||
fun File.ensureDirectory() = if (exists()) isDirectory else mkdirs()
|
||||
|
||||
fun Path.ensureDirectory() = if (exists()) isDirectory else mkdirs().let { true }
|
||||
|
||||
object AppConfig {
|
||||
const val APP_DIRNAME = "EhViewer"
|
||||
private const val DOWNLOAD = "download"
|
||||
private const val TEMP = "temp"
|
||||
private const val PARSE_ERROR = "parse_error"
|
||||
private const val CRASH = "crash"
|
||||
private const val TAG_TRANSLATIONS = "tag-translations"
|
||||
|
||||
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
|
||||
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||
@@ -55,20 +63,18 @@ object AppConfig {
|
||||
|
||||
val defaultDownloadDir: File?
|
||||
get() = getDirInExternalAppDir(DOWNLOAD, false)
|
||||
|
||||
fun getTempDir(filename: String): File? = getDirInExternalAppDir(TEMP)?.run { File(this, filename) }
|
||||
|
||||
val externalTempDir: File?
|
||||
get() = appCtx.externalCacheDir?.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
|
||||
|
||||
val externalTempPersistDir
|
||||
get() = getDirInExternalAppDir(TEMP)?.toOkioPath()
|
||||
val externalParseErrorDir: File?
|
||||
get() = getDirInExternalAppDir(PARSE_ERROR)
|
||||
val externalCrashDir: File?
|
||||
get() = getDirInExternalAppDir(CRASH)
|
||||
val tempDir: File?
|
||||
get() = appCtx.cacheDir.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
|
||||
val tagTranslationsDir
|
||||
get() = (appCtx.filesDir.toOkioPath() / TAG_TRANSLATIONS).apply { check(ensureDirectory()) }
|
||||
|
||||
fun createTempFile(): File? = FileUtils.createTempFile(tempDir, null)
|
||||
|
||||
fun getFilesDir(name: String) = File(appCtx.filesDir, name).takeIf { it.ensureDirectory() }
|
||||
// Following locations will be clear on app startup
|
||||
val tempDir
|
||||
get() = (appCtx.cacheDir.toOkioPath() / TEMP).apply { check(ensureDirectory()) }
|
||||
val externalTempDir
|
||||
get() = appCtx.externalCacheDir?.toOkioPath()?.let { it / TEMP }?.apply { check(ensureDirectory()) }
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import android.os.Build
|
||||
import android.system.Int64Ref
|
||||
import android.system.Os
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hippo.files.inputStream
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.openInputStream
|
||||
import com.hippo.files.openOutputStream
|
||||
import com.hippo.files.outputStream
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.FileDescriptor
|
||||
import okio.Path
|
||||
@@ -27,8 +27,8 @@ infix fun Path.sendTo(file: Path): Long {
|
||||
logcat("sendfile", e)
|
||||
}
|
||||
}
|
||||
return openInputStream().use { src ->
|
||||
file.openOutputStream().use { dst ->
|
||||
return inputStream().use { src ->
|
||||
file.outputStream().use { dst ->
|
||||
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import com.hippo.files.outputStream
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.copyTo
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import okio.Path
|
||||
|
||||
object FileUtils {
|
||||
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||
@@ -51,26 +52,6 @@ object FileUtils {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to delete file, dir and it's children
|
||||
*
|
||||
* @param file the file to delete
|
||||
* The dir to deleted
|
||||
*/
|
||||
fun delete(file: File?): Boolean {
|
||||
file ?: return false
|
||||
return deleteContent(file) and file.delete()
|
||||
}
|
||||
|
||||
fun deleteContent(file: File?): Boolean {
|
||||
file ?: return false
|
||||
var success = true
|
||||
file.listFiles()?.forEach {
|
||||
success = success and delete(it)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun cleanupDirectory(dir: File?, maxFiles: Int = 10) {
|
||||
dir?.listFiles()?.let { files ->
|
||||
files.sortByDescending { it.lastModified() }
|
||||
@@ -142,36 +123,10 @@ object FileUtils {
|
||||
* @return null for start with . dot
|
||||
*/
|
||||
fun getNameFromFilename(filename: String?) = filename?.substringBeforeLast('.')?.ifEmpty { null }
|
||||
|
||||
/**
|
||||
* Create a temp file, you need to delete it by you self.
|
||||
*
|
||||
* @param parent The temp file's parent
|
||||
* @param extension The extension of temp file
|
||||
* @return The temp file or null
|
||||
*/
|
||||
fun createTempFile(parent: File?, extension: String?): File? {
|
||||
parent ?: return null
|
||||
val now = System.currentTimeMillis()
|
||||
for (i in 0..99) {
|
||||
var filename = (now + i).toString()
|
||||
extension?.let {
|
||||
filename = "$filename.$it"
|
||||
}
|
||||
val tempFile = File(parent, filename)
|
||||
if (!tempFile.exists()) {
|
||||
return tempFile
|
||||
}
|
||||
}
|
||||
|
||||
// Unbelievable
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun ByteReadChannel.copyTo(file: File) {
|
||||
RandomAccessFile(file, "rw").use {
|
||||
suspend fun ByteReadChannel.copyTo(file: Path) {
|
||||
file.outputStream().use {
|
||||
copyTo(it.channel)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.provider.DocumentsContractCompat
|
||||
import arrow.core.Either
|
||||
import com.hippo.ehviewer.jni.sha1 as nativeSha1
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.read
|
||||
import com.hippo.files.toUri
|
||||
import kotlinx.io.readString
|
||||
import okio.Path
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -55,4 +58,5 @@ val Path.displayName: String
|
||||
return name
|
||||
}
|
||||
|
||||
fun Path.sha1() = openFileDescriptor("r").use { com.hippo.ehviewer.jni.sha1(it.fd) }
|
||||
fun Path.sha1() = openFileDescriptor("r").use { nativeSha1(it.fd) }
|
||||
fun Path.utf8() = read { readString() }
|
||||
@@ -16,7 +16,8 @@ import okio.FileSystem
|
||||
import okio.IOException
|
||||
import okio.Path
|
||||
import okio.Sink
|
||||
import okio.Source
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
|
||||
class AndroidFileSystem(context: Context) : FileSystem() {
|
||||
@@ -176,12 +177,11 @@ class AndroidFileSystem(context: Context) : FileSystem() {
|
||||
}
|
||||
|
||||
override fun sink(file: Path, mustCreate: Boolean): Sink {
|
||||
TODO("Not yet implemented")
|
||||
if (mustCreate && exists(file)) throw IOException("$file already exists.")
|
||||
return file.outputStream().sink()
|
||||
}
|
||||
|
||||
override fun source(file: Path): Source {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun source(file: Path) = file.inputStream().source()
|
||||
|
||||
fun openFileDescriptor(path: Path, mode: String): ParcelFileDescriptor {
|
||||
if (path.isPhysicalFile()) {
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.os.ParcelFileDescriptor
|
||||
import androidx.core.net.toUri
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import kotlinx.io.Source
|
||||
import kotlinx.io.asSource
|
||||
import kotlinx.io.buffered
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
|
||||
@@ -21,17 +24,21 @@ fun Path.exists() = SystemFileSystem.exists(this)
|
||||
|
||||
fun Path.delete() = SystemFileSystem.deleteRecursively(this)
|
||||
|
||||
fun Path.deleteContent() = SystemFileSystem.listOrNull(this)?.forEach(Path::delete)
|
||||
|
||||
fun Path.list() = SystemFileSystem.listOrNull(this).orEmpty()
|
||||
|
||||
fun Path.mkdirs() = SystemFileSystem.createDirectories(this)
|
||||
|
||||
fun Path.moveTo(target: Path) = SystemFileSystem.atomicMove(this, target)
|
||||
infix fun Path.moveTo(target: Path) = SystemFileSystem.atomicMove(this, target)
|
||||
|
||||
fun Path.openFileDescriptor(mode: String) = SystemFileSystem.openFileDescriptor(this, mode)
|
||||
|
||||
fun Path.openInputStream(): FileInputStream = ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
|
||||
fun Path.inputStream(): FileInputStream = ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
|
||||
|
||||
fun Path.openOutputStream(): FileOutputStream = ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
|
||||
fun Path.outputStream(): FileOutputStream = ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
|
||||
|
||||
inline fun <T> Path.read(f: Source.() -> T) = inputStream().asSource().buffered().use(f)
|
||||
|
||||
fun Path.toUri(): Uri {
|
||||
val str = toString()
|
||||
|
||||
@@ -106,7 +106,7 @@ reorderable = "sh.calvin.reorderable:reorderable:2.4.0"
|
||||
|
||||
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
|
||||
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" }
|
||||
serialization-json-io = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-io", version.ref = "kotlinx-serialization" }
|
||||
|
||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" }
|
||||
|
||||
@@ -126,7 +126,7 @@ androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstall
|
||||
arrow = ["arrow-fx-coroutines", "arrow-functions", "arrow-resilience"]
|
||||
coil = ["coil-compose", "coil-gif", "coil-network"]
|
||||
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-adaptive", "compose-ui-util"]
|
||||
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-okio", "serialization-xml"]
|
||||
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-io", "serialization-xml"]
|
||||
splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences", "splitties-systemservices"]
|
||||
|
||||
[plugins]
|
||||
|
||||
Reference in new issue
Block a user