diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java index 0c7238696..252f807f9 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java @@ -18,35 +18,100 @@ package com.hippo.ehviewer.gallery; import android.content.Context; import android.net.Uri; +import android.os.Process; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.hippo.UriArchiveAccessor; +import com.hippo.ehviewer.GetText; +import com.hippo.ehviewer.R; +import com.hippo.glgallery.GalleryPageView; +import com.hippo.image.Image; import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.thread.PriorityThread; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; public class ArchiveGalleryProvider extends GalleryProvider2 { - public ArchiveGalleryProvider(Context context, Uri uri) { + private static final AtomicInteger sIdGenerator = new AtomicInteger(); + private final UriArchiveAccessor archiveAccessor; + private final Stack requests = new Stack<>(); + private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + private final LinkedHashMap streams = new LinkedHashMap<>(); + private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + private Thread archiveThread; + private Thread decodeThread; + private volatile int size = STATE_WAIT; + private String error; + public ArchiveGalleryProvider(Context context, Uri uri){ + UriArchiveAccessor archiveAccessor1; + try { + archiveAccessor1 = new UriArchiveAccessor(context, uri); + } catch (FileNotFoundException e) { + archiveAccessor1 = null; + e.printStackTrace(); + } + archiveAccessor = archiveAccessor1; } @Override public void start() { super.start(); + + int id = sIdGenerator.incrementAndGet(); + + archiveThread = new PriorityThread( + new ArchiveTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND); + archiveThread.start(); + + decodeThread = new PriorityThread( + new DecodeTask(), "DecodeTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND); + decodeThread.start(); } @Override public void stop() { super.stop(); + + if (archiveThread != null) { + archiveThread.interrupt(); + archiveThread = null; + } + if (decodeThread != null) { + decodeThread.interrupt(); + decodeThread = null; + } } @Override public int size() { - return 0; + return size; } @Override protected void onRequest(int index) { + boolean inDecodeTask; + synchronized (streams) { + inDecodeTask = streams.containsKey(index) || index == decodingIndex.get(); + } + + synchronized (requests) { + boolean inArchiveTask = requests.contains(index) || index == extractingIndex.get(); + if (!inArchiveTask && !inDecodeTask) { + requests.add(index); + requests.notify(); + } + } + notifyPageWait(index); } @Override @@ -56,11 +121,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { @Override protected void onCancelRequest(int index) { + synchronized (requests) { + requests.remove(Integer.valueOf(index)); + } } @Override public String getError() { - return null; + return error; } @NonNull @@ -89,4 +157,94 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { // TODO return Integer.toString(index); } -} + + private class ArchiveTask implements Runnable { + @Override + public void run() { + size = archiveAccessor.open(); + if (size <= 0) { + size = STATE_ERROR; + error = GetText.getString(R.string.error_reading_failed); + notifyDataChanged(); + return; + } + // Update size and notify changed + notifyDataChanged(); + + while (!Thread.currentThread().isInterrupted()) { + int index; + synchronized (requests) { + if (requests.isEmpty()) { + try { + requests.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + index = requests.pop(); + extractingIndex.lazySet(index); + } + + // Check index valid + if (index < 0 || index >= size) { + extractingIndex.lazySet(GalleryPageView.INVALID_INDEX); + notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + + Pipe pipe = new Pipe(32 * 2048); + + synchronized (streams) { + if (streams.get(index) != null) { + continue; + } + streams.put(index, pipe.getInputStream()); + streams.notify(); + } + + archiveAccessor.extractTargetIndexToOutputStream(index, pipe.getOutputStream()); + } + } + } + + private class DecodeTask implements Runnable { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + int index; + InputStream stream; + synchronized (streams) { + if (streams.isEmpty()) { + try { + streams.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + + Iterator> iterator = streams.entrySet().iterator(); + Map.Entry entry = iterator.next(); + iterator.remove(); + index = entry.getKey(); + stream = entry.getValue(); + decodingIndex.lazySet(index); + } + + try { + Image image = Image.decode(stream, true); + if (image != null) { + notifyPageSucceed(index, image); + } else { + notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed)); + } + } finally { + decodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java b/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java index 782a5255d..f591ada86 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java @@ -107,13 +107,6 @@ class Pipe { public void write(@NonNull byte[] b, int off, int len) throws IOException { synchronized (Pipe.this) { while (len != 0) { - if (outClosed) { - throw new IOException("The OutputStream is closed"); - } - if (inClosed) { - throw new IOException("The InputStream is closed"); - } - if (head == tail && full) { // The buffer is full, wait for InputStream read bytes try { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java index 788712edf..f47a91ce5 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -243,8 +243,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan } catch (Exception e) { Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show(); } - throw new Error("UnImplement Feature!!"); - //mGalleryProvider = new ArchiveGalleryProvider(this, mUri); + mGalleryProvider = new ArchiveGalleryProvider(this, mUri); } } } ActivityResultLauncher requestStoragePermissionLauncher = registerForActivityResult( diff --git a/native/src/main/java/com/hippo/UriArchiveAccessor.java b/native/src/main/java/com/hippo/UriArchiveAccessor.java new file mode 100644 index 000000000..d5d3a0f06 --- /dev/null +++ b/native/src/main/java/com/hippo/UriArchiveAccessor.java @@ -0,0 +1,79 @@ +package com.hippo; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +public class UriArchiveAccessor implements Closeable { + public static int OPEN_OK = 1; + public static int OPEN_ERR = 2; + private final OsReadableFile osf; + + public UriArchiveAccessor(Context ctx, Uri uri) throws FileNotFoundException { + osf = new OsReadableFile(ctx, uri); + } + + public int open() { + return openArchive(osf); + } + + public void extractTargetIndexToOutputStream(int index, OutputStream os) { + extracttoOutputStream(osf, index, os); + } + + private native int openArchive(OsReadableFile osf); + private native int extracttoOutputStream(OsReadableFile osf, int index, OutputStream os); + + @Override + public void close() throws IOException { + osf.close(); + } + + static class OsReadableFile { + ParcelFileDescriptor pfd; + FileDescriptor fd; + + public OsReadableFile(Context ctx, Uri uri) throws FileNotFoundException { + pfd = ctx.getContentResolver().openFileDescriptor(uri, "r"); + fd = pfd.getFileDescriptor(); + } + + public int read(byte[] b, int off, int len) throws IOException, ErrnoException { + return Os.read(fd, b, off, len); + } + + public int read(byte[] b) throws IOException, ErrnoException { + return read(b, 0, b.length); + } + + public long length() throws IOException, ErrnoException { + return Os.fstat(fd).st_size; + } + + public void seek(long pos) throws IOException, ErrnoException { + Os.lseek(fd, pos, OsConstants.SEEK_SET); + } + + public void rewind() throws IOException, ErrnoException { + Os.lseek(fd, 0, OsConstants.SEEK_SET); + } + + public void close() throws IOException { + pfd.close(); + try { + Os.close(fd); + } catch (ErrnoException e) { + throw new IOException("An ErrnoException occurs", e); + } + } + } +} diff --git a/native/src/main/jni/CMakeLists.txt b/native/src/main/jni/CMakeLists.txt index be2973c2d..a596a4dbc 100644 --- a/native/src/main/jni/CMakeLists.txt +++ b/native/src/main/jni/CMakeLists.txt @@ -6,7 +6,7 @@ add_subdirectory(image) include_directories(libarchive/contrib/android/include) add_subdirectory(libarchive) -add_library(${PROJECT_NAME} SHARED main.c) +add_library(${PROJECT_NAME} SHARED main.c archive.c) target_include_directories(${PROJECT_NAME} PUBLIC image/image libarchive/libarchive) diff --git a/native/src/main/jni/archive.c b/native/src/main/jni/archive.c new file mode 100644 index 000000000..517d6d47b --- /dev/null +++ b/native/src/main/jni/archive.c @@ -0,0 +1,143 @@ +// +// Created by tarsin on 2022/5/21. +// + +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "libarchive" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +#define MAX_ENTRY_LIMIT 4096 +#define BLOCK_SIZE 10240 + +#define READ_DATA_ERR 3 + +typedef struct archive_inst_sc { + struct archive *archive; + int entry_num; + JNIEnv *env; + jobject file; + jmethodID readID; + jmethodID seekID; + jmethodID skipID; + void* read_buffer; + void* output_buffer; + jbyteArray jbr; +} archive_inst; + +static void archive_list_all_entries(struct archive_inst_sc *inst) +{ + struct archive *a = inst->archive; + struct archive_entry *entry; + int count = 0; + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { + const char* name = archive_entry_pathname(entry); + const char* suffix = name + strlen(name) - 4; + LOGI("%s", name); + if (strcmp(suffix, ".jpg") == 0) + count++; + } + inst->entry_num = count; +} + +ssize_t ins_read(struct archive *a, void* client_data_ptr, const void **buff) +{ + archive_inst *arc = client_data_ptr; + int32_t r = (*arc->env)->CallIntMethod(arc->env, arc->file, arc->readID, arc->jbr); + (*arc->env)->GetByteArrayRegion(arc->env, arc->jbr, 0, r, arc->read_buffer); + *buff = arc->read_buffer; + return r; +} + +int ins_close(struct archive *a, void* client_data) +{ + archive_inst *arc = client_data; + JNIEnv *env = arc->env; + jmethodID rewind = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, arc->file), "rewind", "()V"); + (*arc->env)->CallVoidMethod(arc->env, arc->file, rewind); + return ARCHIVE_OK; +} + +static archive_inst *archive_create_inst(JNIEnv *env, jobject file) +{ + archive_inst *inst = malloc(sizeof(archive_inst)); + inst->archive = archive_read_new(); + archive_read_support_format_all(inst->archive); + inst->env = env; + inst->file = file; + inst->readID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, file), "read", "([B)I"); + inst->jbr = (*env)->NewByteArray(env, BLOCK_SIZE); + inst->read_buffer = malloc(BLOCK_SIZE); + inst->output_buffer = malloc(BLOCK_SIZE); + return inst; +} + +static void archive_destroy_inst(archive_inst* inst) +{ + JNIEnv *env = inst->env; + archive_read_close(inst->archive); + (*env)->DeleteLocalRef(env, inst->jbr); + free(inst->read_buffer); + free(inst->output_buffer); + free(inst); +} + +JNIEXPORT jint JNICALL +Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jobject osf) { + int r; + archive_inst *arc = archive_create_inst(env, osf); + r = archive_read_open(arc->archive, arc, NULL, ins_read, ins_close); + if (r) { + LOGI("%s%d", "Error open inputstream as archive, id is ", r); + r = 0; + } else { + archive_list_all_entries(arc); + r = arc->entry_num; + } + archive_destroy_inst(arc); + return r; +} + +JNIEXPORT jint JNICALL +Java_com_hippo_UriArchiveAccessor_extracttoOutputStream(JNIEnv *env, jobject thiz, jobject osf, + jint index, jobject os) { + int count = 0; + struct archive_entry *entry; + archive_inst *arc = archive_create_inst(env, osf); + int32_t size; + jmethodID writeID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, os), "write", "([B)V"); + jmethodID flushID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, os), "flush", "()V"); + LOGI("%s%d", "Start extract index ", index); + archive_read_open(arc->archive, arc, NULL, ins_read, ins_close); + while (archive_read_next_header(arc->archive, &entry) == ARCHIVE_OK) { + const char* name = archive_entry_pathname(entry); + const char* suffix = name + strlen(name) - 4; + if (strcmp(suffix, ".jpg") != 0) + continue; + if (count == index) { + for (;;) { + size = archive_read_data(arc->archive, arc->output_buffer, BLOCK_SIZE); + LOGI("%s%d%s%d", "index ", index, "size ", size); + if (size < 0) { + return READ_DATA_ERR; + } + if (size == 0) + break; + (*arc->env)->SetByteArrayRegion(env, arc->jbr, 0, size, arc->output_buffer); + (*arc->env)->CallVoidMethod(env, os, writeID, arc->jbr); + (*arc->env)->CallVoidMethod(env, os, flushID); + } + break; + } + count++; + } + LOGI("%s%d", "Stop extract index ", index); + archive_destroy_inst(arc); + return 0; +} \ No newline at end of file