Rewrite ArchivePageLoader with modern kotlin coroutine and flows

This commit is contained in:
tarsin committed 2023-02-21 00:23:24 +08:00
1 parent d9c55a284b
commit 8690f8aa8d
2 files changed
+187 -387

No files matched your search

@@ -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<String>) : 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<Int, Job>()
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<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, Image.CloseableSource> 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<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
}
private Continuation<? super Boolean> continuation = null;
@Nullable
@Override
public Object awaitReady(@NonNull Continuation<? super Boolean> $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<String> 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<Map.Entry<Integer, Image.CloseableSource>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Image.CloseableSource> 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<Int>, pair: Pair<Int, Int>) {}
}
@@ -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<Continuation<String>?> = 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<ReaderActivity>
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"