diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index a52d4be89..01dc40b93 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -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 { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt b/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt index 5f9faa12a..9e77a92ff 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt @@ -65,11 +65,5 @@ fun updateWhenGallerySiteChanges() { } fun updateWhenTagTranslationChanges() { - collectScope.launchIO { - runCatching { - EhTagDatabase.update() - }.onFailure { - logcat(it) - } - } + EhTagDatabase.launchUpdate() } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt index 2a3fd0c9d..b4a3c3875 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt @@ -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 HttpStatement.executeAndParseAs() = executeSafely it.bodyAsUtf8Text().parseAs() } -inline fun BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this) +inline fun Source.parseAs(): T = json.decodeFromSource(this) inline fun String.parseAs(): T = json.decodeFromString(this) val json = Json { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt index 0c47a7f32..ad377ccf1 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt @@ -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> 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? = context.resources.getStringArray(R.array.tag_translation_metadata) + private fun metadata(context: Context): Array? = 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() + } + } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index 068f3b2a3..b8fe2f3b7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -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" } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt b/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt index b1e3b729b..7a03e3772 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt @@ -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() - 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 } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt index fd20c4720..73272d9ba 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt @@ -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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt index 78e88da67..80e71dcd6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -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 { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt index 1d2ba23a3..53a5fb883 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt @@ -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(data.toFile().readBytes()) + data.read { cbor.decodeFromByteArray(readByteArray()) } }.onFailure { logcat(it) }.getOrNull() } fun readCompatFromPath(file: Path): SpiderInfo? = runCatching { - file.openInputStream().use { - cbor.decodeFromByteArray(it.readBytes()) - } + file.read { cbor.decodeFromByteArray(readByteArray()) } }.getOrNull() ?: runCatching { - file.openInputStream().use { readLegacySpiderInfo(it) } + file.readLegacySpiderInfo() }.getOrNull() - -const val TOKEN_FAILED = "failed" diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt index e9a1ec57c..92d9b8455 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt index da5b19afa..cb55927a4 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt index a73bc7d82..ddafbc130 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt @@ -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()) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt b/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt index 06bc77a73..5f180ea8e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt @@ -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) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt index 4e0758e58..12c9a0963 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/AppConfig.kt @@ -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()) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/FDUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/FDUtils.kt index b86e6d011..7f70a58f9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/FDUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/FDUtils.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/FileUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/FileUtils.kt index be75bcbaf..93f1aa41c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/FileUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/FileUtils.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/PathUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/PathUtils.kt index 5cd5d697a..a04799177 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/PathUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/PathUtils.kt @@ -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() } diff --git a/app/src/main/kotlin/com/hippo/files/AndroidFileSystem.kt b/app/src/main/kotlin/com/hippo/files/AndroidFileSystem.kt index 4837a1351..4193ecd9c 100644 --- a/app/src/main/kotlin/com/hippo/files/AndroidFileSystem.kt +++ b/app/src/main/kotlin/com/hippo/files/AndroidFileSystem.kt @@ -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()) { diff --git a/app/src/main/kotlin/com/hippo/files/Paths.kt b/app/src/main/kotlin/com/hippo/files/Paths.kt index 4fac07ad1..9a81a5911 100644 --- a/app/src/main/kotlin/com/hippo/files/Paths.kt +++ b/app/src/main/kotlin/com/hippo/files/Paths.kt @@ -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 Path.read(f: Source.() -> T) = inputStream().asSource().buffered().use(f) fun Path.toUri(): Uri { val str = toString() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5f8bc36c..20ee2d32c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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]