diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index 7bfcb8e8c..166241112 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Hippo Seven + * 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. @@ -13,344 +13,154 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.hippo.ehviewer.gallery -package com.hippo.ehviewer.gallery; +import android.content.Context +import android.net.Uri +import com.hippo.Native.getFd +import com.hippo.UriArchiveAccessor +import com.hippo.ehviewer.Settings +import com.hippo.image.Image +import com.hippo.unifile.UniFile +import com.hippo.yorozuya.FileUtils +import eu.kanade.tachiyomi.util.lang.launchNonCancellable +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.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import java.io.FileOutputStream +import java.io.IOException -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.Process; +class ArchivePageLoader(context: Context, uri: Uri, passwdFlow: Flow) : PageLoader2(), + CoroutineScope { + override val coroutineContext = Dispatchers.IO + Job() + private val archiveAccessor by lazy { UriArchiveAccessor(context, uri) } + private val hostJob = launch(start = CoroutineStart.LAZY) { + size = archiveAccessor.open() + if (size == 0) { + return@launch + } + archiveAccessor.run { + if (needPassword()) { + Settings.getArchivePasswds()?.forEach { + if (providePassword(it)) return@launch + } + passwdFlow.collect { + if (providePassword(it)) { + Settings.putPasswdToArchivePasswds(it) + currentCoroutineContext().cancel() + } + } + } + } + } -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; + override var size = 0 + private set -import com.hippo.Native; -import com.hippo.UriArchiveAccessor; -import com.hippo.ehviewer.GetText; -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.Settings; -import com.hippo.image.Image; -import com.hippo.unifile.UniFile; -import com.hippo.yorozuya.FileUtils; -import com.hippo.yorozuya.SimpleHandler; -import com.hippo.yorozuya.thread.PVLock; -import com.hippo.yorozuya.thread.PriorityThread; + override fun start() { + hostJob.start() + } -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Stack; -import java.util.concurrent.atomic.AtomicInteger; + override fun stop() { + cancel() + launchNonCancellable { + mJobMap.values.joinAll() + archiveAccessor.close() + } + super.stop() + } -import kotlin.Pair; -import kotlin.coroutines.Continuation; -import kotlin.coroutines.intrinsics.CoroutineSingletons; + private val mJobMap = hashMapOf() + private val mSemaphore = Semaphore(4) -public class ArchivePageLoader extends PageLoader2 { - private static final AtomicInteger sIdGenerator = new AtomicInteger(); - public static Handler showPasswd; - public static String passwd; - public static PVLock pv = new PVLock(0); - private final UriArchiveAccessor archiveAccessor; - private final Stack requests = new Stack<>(); - private final LinkedHashMap streams = new LinkedHashMap<>(); - private final Thread[] decodeThread = new Thread[]{ - new Thread(new DecodeTask()), - new Thread(new DecodeTask()), - new Thread(new DecodeTask()), - new Thread(new DecodeTask()) - }; - private Thread archiveThread; - private volatile int size = 0; - private String error; + override fun onRequest(index: Int) { + notifyPageWait(index) + synchronized(mJobMap) { + val current = mJobMap[index] + if (current?.isActive != true) { + mJobMap[index] = launch { + mSemaphore.withPermit { + doRealWork(index) + } + } + } + } + } - public ArchivePageLoader(Context context, Uri uri) { - UriArchiveAccessor archiveAccessor1; + private suspend fun doRealWork(index: Int) { + val src = archiveAccessor.getImageSource(index) ?: return + 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) { + onRequest(index) + } + + override suspend fun awaitReady(): Boolean { + hostJob.join() + return 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(archiveAccessor.getFilename(index)) + } + + override fun save(index: Int, file: UniFile): Boolean { + val fd: Int + val stream: FileOutputStream try { - archiveAccessor1 = new UriArchiveAccessor(context, uri); - } catch (Exception e) { - archiveAccessor1 = null; - e.printStackTrace(); - } - archiveAccessor = archiveAccessor1; - } - - @Override - public void start() { - int id = sIdGenerator.incrementAndGet(); - - archiveThread = new PriorityThread( - new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND); - archiveThread.start(); - for (Thread i : decodeThread) { - i.start(); - } - } - - @Override - public void stop() { - super.stop(); - if (archiveThread != null) { - archiveThread.interrupt(); - archiveThread = null; - } - for (Thread i : decodeThread) { - i.interrupt(); + stream = file.openOutputStream() as FileOutputStream + fd = getFd(stream.fd) + } catch (e: IOException) { + e.printStackTrace() + return false } + archiveAccessor.extractToFd(index, fd) try { - archiveAccessor.close(); - } catch (Exception e) { - e.printStackTrace(); + stream.close() + } catch (e: IOException) { + e.printStackTrace() + return false } + return true } - @Override - public int getSize() { - return size; + override fun save(index: Int, dir: UniFile, filename: String): UniFile { + val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index)) + val dst = dir.subFile(if (null != extension) "$filename.$extension" else filename) + save(index, dst!!) + return dst } - @Override - protected void onRequest(int index) { - boolean inDecodeTask; - synchronized (streams) { - inDecodeTask = streams.containsKey(index); - } - - synchronized (requests) { - boolean inArchiveTask = requests.contains(index); - if (!inArchiveTask && !inDecodeTask) { - requests.add(index); - requests.notify(); - } - } - notifyPageWait(index); - } - - @Override - protected void onForceRequest(int index) { - onRequest(index); - } - - @Override - protected void onCancelRequest(int index) { - synchronized (requests) { - requests.remove(Integer.valueOf(index)); - } - } - - @NonNull - @Override - public String getImageFilename(int index) { - return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index)); - } - - @NonNull - @Override - public String getImageFilenameWithExtension(int index) { - return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index)); - } - - @Override - public boolean save(int index, @NonNull UniFile file) { - int fd; - FileOutputStream stream; - try { - stream = (FileOutputStream) file.openOutputStream(); - fd = Native.getFd(stream.getFD()); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - archiveAccessor.extractToFd(index, fd); - try { - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } - - @Nullable - @Override - public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) { - String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index)); - UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename); - save(index, dst); - return dst; - } - - @Override - protected void preloadPages(@NonNull List pages, @NonNull Pair pair) { - - } - - private Continuation continuation = null; - - @Nullable - @Override - public Object awaitReady(@NonNull Continuation $completion) { - continuation = $completion; - return CoroutineSingletons.COROUTINE_SUSPENDED; - } - - private class ArchiveHostTask implements Runnable { - public final Thread[] archiveThreads = new Thread[]{ - new Thread(new ArchiveTask()), - new Thread(new ArchiveTask()), - new Thread(new ArchiveTask()), - new Thread(new ArchiveTask()) - }; - - private void waitPasswd() throws InterruptedException { - showPasswd.sendEmptyMessage(0); - pv.p(); - } - - private void notifyError() { - showPasswd.sendEmptyMessage(1); - } - - private void notifyDismiss() { - showPasswd.sendEmptyMessage(2); - } - - @Override - public void run() { - try { - size = archiveAccessor.open(); - } catch (Exception e) { - e.printStackTrace(); - size = 0; - } - if (size <= 0) { - error = GetText.getString(R.string.error_reading_failed); - return; - } - SimpleHandler.getInstance().post(() -> continuation.resumeWith(true)); - - - if (archiveAccessor.needPassword()) { - boolean need_request = true; - Set set = Settings.getArchivePasswds(); - if (set != null) { - for (String passwd : set) { - if (archiveAccessor.providePassword(passwd)) { - need_request = false; - break; - } - } - } - while (!Thread.currentThread().isInterrupted() && need_request) { - try { - waitPasswd(); - } catch (InterruptedException e) { - e.printStackTrace(); - return; - } - if (!archiveAccessor.providePassword(passwd)) - notifyError(); - else { - Settings.putPasswdToArchivePasswds(passwd); - notifyDismiss(); - break; - } - } - } - for (Thread i : archiveThreads) { - i.start(); - } - - Object o = new Object(); - synchronized (o) { - try { - o.wait(); - } catch (InterruptedException e) { - for (Thread i : archiveThreads) { - i.interrupt(); - } - } - } - } - } - - private class ArchiveTask implements Runnable { - @Override - public void run() { - while (!Thread.currentThread().isInterrupted()) { - int index; - synchronized (requests) { - if (requests.isEmpty()) { - try { - requests.wait(); - } catch (InterruptedException e) { - // Interrupted - break; - } - continue; - } - index = requests.pop(); - } - - // Check index valid - if (index < 0 || index >= size) { - notifyPageFailed(index, null); - continue; - } - - synchronized (streams) { - if (streams.get(index) != null) { - continue; - } - } - - Image.CloseableSource src = archiveAccessor.getImageSource(index); - - synchronized (streams) { - streams.put(index, src); - streams.notify(); - } - } - } - } - - private class DecodeTask implements Runnable { - @Override - public void run() { - while (!Thread.currentThread().isInterrupted()) { - int index; - Image.CloseableSource src; - synchronized (streams) { - if (streams.isEmpty()) { - try { - streams.wait(); - } catch (InterruptedException e) { - break; - } - continue; - } - - Iterator> iterator = streams.entrySet().iterator(); - Map.Entry entry = iterator.next(); - iterator.remove(); - index = entry.getKey(); - src = entry.getValue(); - } - - Image image = null; - if (src != null) { - image = Image.decode(src); - } - if (image != null) { - notifyPageSucceed(index, image); - } else { - notifyPageFailed(index, null); - } - } - } - } + override fun preloadPages(pages: List, pair: Pair) {} } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 1e3ffeb0c..c3f52c60e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -33,8 +33,6 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment -import android.os.Handler -import android.os.Message import android.provider.MediaStore import android.text.TextUtils import android.view.HapticFeedbackConstants @@ -88,6 +86,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.preference.toggle import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.hasDisplayCutout @@ -97,16 +96,22 @@ import eu.kanade.tachiyomi.util.view.popupMenu import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.suspendCancellableCoroutine import java.io.File import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume import kotlin.math.abs import kotlin.math.max @@ -161,9 +166,9 @@ class ReaderActivity : EhActivity() { private var mSize: Int = 0 private var mCurrentIndex: Int = 0 private var mSavingPage = -1 - private var builder: EditTextDialogBuilder? = null + private lateinit var builder: EditTextDialogBuilder private var dialogShown = false - private var dialog: AlertDialog? = null + private lateinit var dialog: AlertDialog private var requestStoragePermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { result -> @@ -207,8 +212,41 @@ class ReaderActivity : EhActivity() { Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show() } - mGalleryProvider = - ArchivePageLoader(this, mUri) + val continuation: AtomicReference?> = AtomicReference(null) + mGalleryProvider = ArchivePageLoader(this, mUri!!, + flow { + if (!dialogShown) { + withUIContext { + dialogShown = true + dialog.run { + show() + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val passwd = builder.text + if (passwd.isEmpty()) + builder.setError(getString(R.string.passwd_cannot_be_empty)) + else { + continuation.get()?.resume(passwd) + } + } + setOnCancelListener { + finish() + } + } + } + } + while (true) { + currentCoroutineContext().ensureActive() + val r = suspendCancellableCoroutine { + continuation.set(it) + it.invokeOnCancellation { dialog.dismiss() } + } + emit(r) + withUIContext { + builder.setError(getString(R.string.passwd_wrong)) + } + } + } + ) } } } @@ -258,15 +296,14 @@ class ReaderActivity : EhActivity() { binding = ReaderActivityBinding.inflate(layoutInflater) setContentView(binding.root) builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd)) - builder!!.setTitle(getString(R.string.archive_need_passwd)) - builder!!.setPositiveButton(getString(android.R.string.ok), null) - dialog = builder!!.create() - dialog!!.setCanceledOnTouchOutside(false) + builder.setTitle(getString(R.string.archive_need_passwd)) + builder.setPositiveButton(getString(android.R.string.ok), null) + dialog = builder.create() + dialog.setCanceledOnTouchOutside(false) if (mGalleryProvider == null) { finish() return } - ArchivePageLoader.showPasswd = ShowPasswdDialogHandler(this) mGalleryProvider!!.start() @@ -279,6 +316,9 @@ class ReaderActivity : EhActivity() { } fun setGallery() { + // TODO: Not well place to call it + dialog.dismiss() + // Get start page if (mCurrentIndex == 0) mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage mSize = mGalleryProvider!!.size @@ -520,56 +560,6 @@ class ReaderActivity : EhActivity() { } } - private fun showPasswdDialog() { - if (!dialogShown) { - dialogShown = true - dialog!!.show() - if (dialog!!.getButton(AlertDialog.BUTTON_POSITIVE) != null) { - dialog!!.getButton(AlertDialog.BUTTON_POSITIVE) - .setOnClickListener { this@ReaderActivity.onProvidePasswd() } - } - dialog!!.setOnCancelListener { finish() } - } - } - - private fun onProvidePasswd() { - val passwd = builder!!.text - if (passwd.isEmpty()) - builder!!.setError(getString(R.string.passwd_cannot_be_empty)) - else { - ArchivePageLoader.passwd = passwd - ArchivePageLoader.pv.v() - } - } - - private fun onPasswdWrong() { - builder!!.setError(getString(R.string.passwd_wrong)) - } - - private fun onPasswdCorrect() { - dialog!!.dismiss() - } - - private class ShowPasswdDialogHandler(activity: ReaderActivity) : Handler() { - private val weakReference: WeakReference - - init { - this.weakReference = WeakReference(activity) - } - - override fun handleMessage(msg: Message) { - val activity = weakReference.get() - super.handleMessage(msg) - if (null != activity) { - when (msg.what) { - 0 -> activity.showPasswdDialog() - 1 -> activity.onPasswdWrong() - 2 -> activity.onPasswdCorrect() - } - } - } - } - companion object { const val ACTION_EH = "eh" const val KEY_ACTION = "action"