Files
EhViewer/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.kt
T
2023-07-12 07:48:21 +08:00

186 lines
5.8 KiB
Kotlin

/*
* Copyright 2023 Tarsin Norbin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery
import android.content.Context
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import com.hippo.ehviewer.Settings.archivePasswds
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.image.rewriteGifSource
import com.hippo.ehviewer.util.FileUtils
import com.hippo.unifile.UniFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.NamedMutex
import moe.tarsin.coroutines.withLock
import java.nio.ByteBuffer
class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow<String>) : PageLoader2(), CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private lateinit var pfd: ParcelFileDescriptor
private val hostJob = launch(start = CoroutineStart.LAZY) {
Log.d(DEBUG_TAG, "Open archive $uri")
pfd = context.contentResolver.openFileDescriptor(uri, "r")!!
size = openArchive(pfd.fd, pfd.statSize)
if (size == 0) {
return@launch
}
if (needPassword()) {
archivePasswds?.forEach {
it ?: return@forEach
if (providePassword(it)) return@launch
}
passwdFlow.collect {
if (providePassword(it)) {
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(it) } ?: setOf(it)
currentCoroutineContext().cancel()
}
}
}
}
override var size = 0
private set
override fun start() {
hostJob.start()
}
override fun stop() {
cancel()
closeArchive()
pfd.close()
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
super.stop()
}
private val mJobMap = hashMapOf<Int, Job>()
private val mWorkerMutex = NamedMutex<Int>()
private val mSemaphore = Semaphore(4)
override fun onRequest(index: Int) {
synchronized(mJobMap) {
val current = mJobMap[index]
if (current?.isActive != true) {
mJobMap[index] = launch {
mWorkerMutex.withLock(index) {
mSemaphore.withPermit {
doRealWork(index)
}
}
}
}
}
}
private suspend fun doRealWork(index: Int) {
val buffer = extractToByteBuffer(index)
buffer ?: return
check(buffer.isDirect)
rewriteGifSource(buffer)
val source = ImageDecoder.createSource(buffer)
val src = object : Image.CloseableSource {
override val source: ImageDecoder.Source
get() = source
override fun close() {
releaseByteBuffer(buffer)
}
}
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
src.close()
throw it
}
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image.recycle()
throw it
}
notifyPageSucceed(index, image)
}
override fun onForceRequest(index: Int, orgImg: Boolean) {
onRequest(index)
}
override suspend fun awaitReady(): Boolean {
hostJob.join()
return size != 0
}
override val isReady: Boolean
get() = size != 0
override fun onCancelRequest(index: Int) {
mJobMap[index]?.cancel()
}
override fun getImageFilename(index: Int): String {
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))!!
}
override fun getImageFilenameWithExtension(index: Int): String {
return FileUtils.sanitizeFilename(getFilename(index))
}
override fun save(index: Int, file: UniFile): Boolean {
runCatching {
file.openFileDescriptor("w").use {
extractToFd(index, it.fd)
}
}.onFailure {
it.printStackTrace()
return false
}
return true
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
val dst = dir.subFile(filename)
save(index, dst!!)
return dst
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
}
private const val DEBUG_TAG = "ArchivePageLoader"
private external fun releaseByteBuffer(buffer: ByteBuffer)
private external fun openArchive(fd: Int, size: Long): Int
private external fun extractToByteBuffer(index: Int): ByteBuffer?
private external fun extractToFd(index: Int, fd: Int)
private external fun getFilename(index: Int): String
private external fun needPassword(): Boolean
private external fun providePassword(str: String): Boolean
private external fun closeArchive()