ag2s20150909 há 3 anos atrás
pai
commit
8bd5ce8a4e

+ 17 - 0
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -1,5 +1,6 @@
 package io.legado.app.help.book
 
+import android.os.ParcelFileDescriptor
 import androidx.documentfile.provider.DocumentFile
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
@@ -198,6 +199,22 @@ object BookHelp {
         return ZipFile(uri.path)
     }
 
+    /**
+     * 获取本地书籍文件的ParcelFileDescriptor
+     *
+     * @param book
+     * @return
+     */
+    @Throws(IOException::class, FileNotFoundException::class)
+    fun getBookPFD(book: Book): ParcelFileDescriptor? {
+        val uri = book.getLocalUri()
+        return if (uri.isContentScheme()) {
+            appCtx.contentResolver.openFileDescriptor(uri, "r")
+        } else {
+            ParcelFileDescriptor.open(File(uri.path), ParcelFileDescriptor.MODE_READ_ONLY)
+        }
+    }
+
     fun getChapterFiles(book: Book): HashSet<String> {
         val fileNames = hashSetOf<String>()
         if (book.isLocalTxt) {

+ 2 - 0
app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt

@@ -8,10 +8,12 @@ import com.bumptech.glide.load.model.GlideUrl
 import com.bumptech.glide.module.AppGlideModule
 import java.io.InputStream
 
+
 @Suppress("unused")
 @GlideModule
 class LegadoGlideModule : AppGlideModule() {
 
+
     override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
         registry.replace(
             GlideUrl::class.java,

+ 21 - 3
app/src/main/java/io/legado/app/model/localBook/EpubFile.kt

@@ -2,6 +2,7 @@ package io.legado.app.model.localBook
 
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
+import android.os.ParcelFileDescriptor
 import android.text.TextUtils
 import io.legado.app.constant.AppLog
 import io.legado.app.data.entities.Book
@@ -12,6 +13,7 @@ import me.ag2s.epublib.domain.EpubBook
 import me.ag2s.epublib.domain.Resource
 import me.ag2s.epublib.domain.TOCReference
 import me.ag2s.epublib.epub.EpubReader
+import me.ag2s.epublib.util.zip.AndroidZipFile
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Element
 import org.jsoup.select.Elements
@@ -65,9 +67,14 @@ class EpubFile(var book: Book) {
     }
 
     private var mCharset: Charset = Charset.defaultCharset()
+
+    /**
+     *持有引用,避免被回收
+     */
+    private var fileDescriptor: ParcelFileDescriptor? = null
     private var epubBook: EpubBook? = null
         get() {
-            if (field != null) {
+            if (field != null && fileDescriptor != null) {
                 return field
             }
             field = readEpub()
@@ -107,8 +114,14 @@ class EpubFile(var book: Book) {
     private fun readEpub(): EpubBook? {
         return kotlin.runCatching {
             //ContentScheme拷贝到私有文件夹采用懒加载防止OOM
-            val zipFile = BookHelp.getEpubFile(book)
-            EpubReader().readEpubLazy(zipFile, "utf-8")
+            //val zipFile = BookHelp.getEpubFile(book)
+            BookHelp.getBookPFD(book)?.let {
+                fileDescriptor = it
+                val zipFile = AndroidZipFile(it, book.originName)
+                EpubReader().readEpubLazy(zipFile, "utf-8")
+            }
+
+
         }.onFailure {
             AppLog.put("读取Epub文件失败\n${it.localizedMessage}", it)
             it.printOnDebug()
@@ -350,4 +363,9 @@ class EpubFile(var book: Book) {
         }
     }
 
+
+    protected fun finalize() {
+        fileDescriptor?.close()
+    }
+
 }

+ 5 - 2
app/src/main/java/io/legado/app/model/localBook/PdfFile.kt

@@ -57,10 +57,13 @@ class PdfFile(var book: Book) {
 
     }
 
+    /**
+     *持有引用,避免被回收
+     */
     private var fileDescriptor: ParcelFileDescriptor? = null
     private var pdfRenderer: PdfRenderer? = null
         get() {
-            if (field != null) {
+            if (field != null && fileDescriptor != null) {
                 return field
             }
             field = readPdf()
@@ -180,7 +183,7 @@ class PdfFile(var book: Book) {
             val index = href.toInt()
             val bitmap = openPdfPage(pdfRenderer!!, index)
             if (bitmap != null) {
-                BitmapUtils.toInputStream(bitmap).also { bitmap.recycle() }
+                BitmapUtils.toInputStream(bitmap)
             } else {
                 null
             }

+ 102 - 0
epublib/src/main/java/me/ag2s/base/PfdHelper.java

@@ -0,0 +1,102 @@
+package me.ag2s.base;
+
+import static me.ag2s.base.ThrowableUtils.rethrowAsIOException;
+
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * 读取ParcelFileDescriptor的工具类
+ */
+@SuppressWarnings("unused")
+public final class PfdHelper {
+
+    /**
+     * 读取基本类型的buffer
+     */
+    private static final byte[] readBuffer = new byte[8];
+
+    public static void seek(ParcelFileDescriptor pfd, long pos) throws IOException {
+        try {
+            android.system.Os.lseek(pfd.getFileDescriptor(), pos, OsConstants.SEEK_SET);
+        } catch (ErrnoException e) {
+            throw rethrowAsIOException(e);
+        }
+
+    }
+
+    public static long getFilePointer(ParcelFileDescriptor pfd) throws IOException {
+        try {
+            return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_CUR);
+        } catch (ErrnoException e) {
+            throw rethrowAsIOException(e);
+        }
+    }
+
+    public static long length(ParcelFileDescriptor pfd) throws IOException {
+        try {
+            return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_END);
+        } catch (ErrnoException e) {
+            throw rethrowAsIOException(e);
+        }
+    }
+
+    private static int readBytes(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
+        try {
+            return android.system.Os.read(pfd.getFileDescriptor(), b, off, len);
+        } catch (ErrnoException e) {
+            throw rethrowAsIOException(e);
+        }
+    }
+
+    public static int read(ParcelFileDescriptor pfd) throws IOException {
+        return (read(pfd, readBuffer, 0, 1) != -1) ? readBuffer[0] & 0xff : -1;
+    }
+
+    public static int read(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
+        return readBytes(pfd, b, off, len);
+    }
+
+    public static int read(ParcelFileDescriptor pfd, byte[] b) throws IOException {
+        return readBytes(pfd, b, 0, b.length);
+    }
+
+    public static void readFully(ParcelFileDescriptor pfd, byte[] b) throws IOException {
+        readFully(pfd, b, 0, b.length);
+    }
+
+    public static void readFully(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
+        int n = 0;
+        do {
+            int count = read(pfd, b, off + n, len - n);
+            if (count < 0)
+                throw new EOFException();
+            n += count;
+        } while (n < len);
+    }
+
+
+    public static int skipBytes(ParcelFileDescriptor pfd, int n) throws IOException {
+        long pos;
+        long len;
+        long newpos;
+
+        if (n <= 0) {
+            return 0;
+        }
+        pos = getFilePointer(pfd);
+        len = length(pfd);
+        newpos = pos + n;
+        if (newpos > len) {
+            newpos = len;
+        }
+        seek(pfd, newpos);
+
+        /* return the actual number of bytes skipped */
+        return (int) (newpos - pos);
+    }
+}

+ 16 - 0
epublib/src/main/java/me/ag2s/base/ThrowableUtils.java

@@ -0,0 +1,16 @@
+package me.ag2s.base;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+
+public class ThrowableUtils {
+
+
+    public static @NonNull
+    IOException rethrowAsIOException(Throwable throwable) throws IOException {
+        IOException newException = new IOException(throwable.getMessage());
+        newException.initCause(throwable);
+        throw newException;
+    }
+}

+ 11 - 10
epublib/src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java

@@ -4,8 +4,9 @@ import androidx.annotation.NonNull;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
+
+import me.ag2s.epublib.util.zip.ZipEntryWrapper;
+import me.ag2s.epublib.util.zip.ZipFileWrapper;
 
 /**
  * @author jake
@@ -13,24 +14,24 @@ import java.util.zip.ZipFile;
 public class EpubResourceProvider implements LazyResourceProvider {
 
 
-  private final String epubFilename;
+  private final ZipFileWrapper zipFileWrapper;
 
 
-  public EpubResourceProvider(String epubFilename) {
-    this.epubFilename = epubFilename;
+  public EpubResourceProvider(ZipFileWrapper zipFileWrapper) {
+    this.zipFileWrapper = zipFileWrapper;
   }
 
 
   @Override
   public InputStream getResourceStream(@NonNull String href) throws IOException {
 
-    ZipFile zipFile = new ZipFile(epubFilename);
-    ZipEntry zipEntry = zipFile.getEntry(href);
+    //ZipFile zipFile = new ZipFile(epubFilename);
+    ZipEntryWrapper zipEntry = zipFileWrapper.getEntry(href);
     if (zipEntry == null) {
-      zipFile.close();
+      //zipFile.close();
       throw new IllegalStateException(
-              "Cannot find entry " + href + " in epub file " + epubFilename);
+              "Cannot find entry " + href + " in epub file " + zipFileWrapper);
     }
-    return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile);
+    return new ResourceInputStream(zipFileWrapper.getInputStream(zipEntry));
   }
 }

+ 14 - 17
epublib/src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java

@@ -3,34 +3,31 @@ package me.ag2s.epublib.domain;
 import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.zip.ZipFile;
 
 /**
- * A wrapper class for closing a ZipFile object when the InputStream derived
+ * A wrapper class for closing a AndroidZipFile object when the InputStream derived
  * from it is closed.
  *
  * @author ttopalov
  */
 public class ResourceInputStream extends FilterInputStream {
 
-  private final ZipFile zipFile;
+    //private final ZipFile zipFile;
 
-  /**
-   * Constructor.
-   *
-   * @param in
-   *            The InputStream object.
-   * @param zipFile
-   *            The ZipFile object.
-   */
-  public ResourceInputStream(InputStream in, ZipFile zipFile) {
-    super(in);
-    this.zipFile = zipFile;
-  }
+    /**
+     * Constructor.
+     *
+     * @param in The InputStream object.
+     */
+    public ResourceInputStream(InputStream in) {
+        super(in);
+        //this.zipFile = zipFile;
+    }
 
   @Override
   public void close() throws IOException {
-    super.close();
-    zipFile.close();
+      super.close();
+
+      //zipFile.close();
   }
 }

+ 16 - 2
epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java

@@ -22,6 +22,8 @@ import me.ag2s.epublib.domain.Resource;
 import me.ag2s.epublib.domain.Resources;
 import me.ag2s.epublib.util.ResourceUtil;
 import me.ag2s.epublib.util.StringUtil;
+import me.ag2s.epublib.util.zip.AndroidZipFile;
+import me.ag2s.epublib.util.zip.ZipFileWrapper;
 
 /**
  * Reads an epub file.
@@ -72,12 +74,17 @@ public class EpubReader {
         return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
     }
 
+    public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding)
+            throws IOException {
+        return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
+    }
+
     public EpubBook readEpub(@NonNull ZipInputStream in, @NonNull String encoding) throws IOException {
         return readEpub(ResourcesLoader.loadResources(in, encoding));
     }
 
     public EpubBook readEpub(ZipFile in, String encoding) throws IOException {
-        return readEpub(ResourcesLoader.loadResources(in, encoding));
+        return readEpub(ResourcesLoader.loadResources(new ZipFileWrapper(in), encoding));
     }
 
     /**
@@ -92,7 +99,14 @@ public class EpubReader {
     public EpubBook readEpubLazy(@NonNull ZipFile zipFile, @NonNull String encoding,
                                  @NonNull List<MediaType> lazyLoadedTypes) throws IOException {
         Resources resources = ResourcesLoader
-                .loadResources(zipFile, encoding, lazyLoadedTypes);
+                .loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
+        return readEpub(resources);
+    }
+
+    public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding,
+                                 @NonNull List<MediaType> lazyLoadedTypes) throws IOException {
+        Resources resources = ResourcesLoader
+                .loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
         return readEpub(resources);
     }
 

+ 12 - 11
epublib/src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java

@@ -9,7 +9,6 @@ import java.util.Enumeration;
 import java.util.List;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
 import java.util.zip.ZipInputStream;
 
 import me.ag2s.epublib.domain.EpubResourceProvider;
@@ -21,6 +20,8 @@ import me.ag2s.epublib.domain.Resource;
 import me.ag2s.epublib.domain.Resources;
 import me.ag2s.epublib.util.CollectionUtil;
 import me.ag2s.epublib.util.ResourceUtil;
+import me.ag2s.epublib.util.zip.ZipEntryWrapper;
+import me.ag2s.epublib.util.zip.ZipFileWrapper;
 
 
 /**
@@ -34,32 +35,32 @@ public class ResourcesLoader {
 
 
     /**
-     * Loads the entries of the zipFile as resources.
+     * Loads the entries of the zipFileWrapper as resources.
      * <p>
      * The MediaTypes that are in the lazyLoadedTypes will not get their
      * contents loaded, but are stored as references to entries into the
-     * ZipFile and are loaded on demand by the Resource system.
+     * AndroidZipFile and are loaded on demand by the Resource system.
      *
-     * @param zipFile             import epub zipfile
+     * @param zipFileWrapper      import epub zipfile
      * @param defaultHtmlEncoding epub xhtml default encoding
      * @param lazyLoadedTypes     lazyLoadedTypes
      * @return Resources
      * @throws IOException IOException
      */
     public static Resources loadResources(
-            ZipFile zipFile,
+            ZipFileWrapper zipFileWrapper,
             String defaultHtmlEncoding,
             List<MediaType> lazyLoadedTypes
     ) throws IOException {
 
         LazyResourceProvider resourceProvider =
-                new EpubResourceProvider(zipFile.getName());
+                new EpubResourceProvider(zipFileWrapper);
 
         Resources result = new Resources();
-        Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        Enumeration entries = zipFileWrapper.entries();
 
         while (entries.hasMoreElements()) {
-            ZipEntry zipEntry = entries.nextElement();
+            ZipEntryWrapper zipEntry = new ZipEntryWrapper(entries.nextElement());
 
             if (zipEntry == null || zipEntry.isDirectory()) {
                 continue;
@@ -73,7 +74,7 @@ public class ResourcesLoader {
                 resource = new LazyResource(resourceProvider, zipEntry.getSize(), href);
             } else {
                 resource = ResourceUtil
-                        .createResource(zipEntry, zipFile.getInputStream(zipEntry));
+                        .createResource(zipEntry.getName(), zipFileWrapper.getInputStream(zipEntry));
                 /*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
                 if (href.endsWith("opf")) {
                     String string = new String(resource.getData())
@@ -136,7 +137,7 @@ public class ResourcesLoader {
             String href = zipEntry.getName();
 
             // store resource
-            Resource resource = ResourceUtil.createResource(zipEntry, zipInputStream);
+            Resource resource = ResourceUtil.createResource(zipEntry.getName(), zipInputStream);
             ///*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
             if (href.endsWith("opf")) {
                 String string = new String(resource.getData())
@@ -184,7 +185,7 @@ public class ResourcesLoader {
      * @return Resources
      * @throws IOException IOException
      */
-    public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding) throws IOException {
+    public static Resources loadResources(ZipFileWrapper zipFile, String defaultHtmlEncoding) throws IOException {
         List<MediaType> ls = new ArrayList<>();
         return loadResources(zipFile, defaultHtmlEncoding, ls);
     }

+ 5 - 6
epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java

@@ -10,7 +10,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
-import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -102,20 +101,20 @@ public class ResourceUtil {
     /**
      * Creates a resource out of the given zipEntry and zipInputStream.
      *
-     * @param zipEntry       v
+     * @param name           v
      * @param zipInputStream v
      * @return a resource created out of the given zipEntry and zipInputStream.
      * @throws IOException v
      */
-    public static Resource createResource(ZipEntry zipEntry,
+    public static Resource createResource(String name,
                                           ZipInputStream zipInputStream) throws IOException {
-        return new Resource(zipInputStream, zipEntry.getName());
+        return new Resource(zipInputStream, name);
 
     }
 
-    public static Resource createResource(ZipEntry zipEntry,
+    public static Resource createResource(String name,
                                           InputStream zipInputStream) throws IOException {
-        return new Resource(zipInputStream, zipEntry.getName());
+        return new Resource(zipInputStream, name);
 
     }
 

+ 355 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/AndroidZipEntry.java

@@ -0,0 +1,355 @@
+
+
+
+package me.ag2s.epublib.util.zip;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * This class represents a member of a zip archive.  ZipFile and
+ * ZipInputStream will give you instances of this class as information
+ * about the members in an archive.  On the other hand ZipOutputStream
+ * needs an instance of this class to create a new member.
+ *
+ * @author Jochen Hoenicke
+ */
+public class AndroidZipEntry implements ZipConstants, Cloneable {
+  private static final int KNOWN_SIZE = 1;
+  private static final int KNOWN_CSIZE = 2;
+  private static final int KNOWN_CRC = 4;
+  private static final int KNOWN_TIME = 8;
+
+  private static Calendar cal;
+
+  private final String name;
+  private int size;
+  private int compressedSize;
+  private int crc;
+  private int dostime;
+  private short known = 0;
+  private short method = -1;
+  private byte[] extra = null;
+  private String comment = null;
+
+  int flags;              /* used by ZipOutputStream */
+  int offset;             /* used by ZipFile and ZipOutputStream */
+
+
+  /**
+   * Compression method.  This method doesn't compress at all.
+   */
+  public final static int STORED = 0;
+  /**
+   * Compression method.  This method uses the Deflater.
+   */
+  public final static int DEFLATED = 8;
+
+  /**
+   * Creates a zip entry with the given name.
+   *
+   * @param name the name. May include directory components separated
+   *             by '/'.
+   * @throws NullPointerException     when name is null.
+   * @throws IllegalArgumentException when name is bigger then 65535 chars.
+   */
+  public AndroidZipEntry(String name) {
+    int length = name.length();
+    if (length > 65535)
+      throw new IllegalArgumentException("name length is " + length);
+    this.name = name;
+  }
+
+  /**
+   * Creates a copy of the given zip entry.
+   *
+   * @param e the entry to copy.
+   */
+  public AndroidZipEntry(AndroidZipEntry e) {
+    name = e.name;
+    known = e.known;
+    size = e.size;
+    compressedSize = e.compressedSize;
+    crc = e.crc;
+    dostime = e.dostime;
+    method = e.method;
+    extra = e.extra;
+    comment = e.comment;
+  }
+
+  final void setDOSTime(int dostime) {
+    this.dostime = dostime;
+    known |= KNOWN_TIME;
+  }
+
+  final int getDOSTime() {
+    if ((known & KNOWN_TIME) == 0)
+      return 0;
+    else
+      return dostime;
+  }
+
+  /**
+   * Creates a copy of this zip entry.
+   */
+  /**
+   * Clones the entry.
+   */
+  public Object clone() {
+    try {
+      // The JCL says that the `extra' field is also copied.
+      AndroidZipEntry clone = (AndroidZipEntry) super.clone();
+      if (extra != null)
+        clone.extra = (byte[]) extra.clone();
+      return clone;
+    } catch (CloneNotSupportedException ex) {
+      throw new InternalError();
+    }
+  }
+
+  /**
+   * Returns the entry name.  The path components in the entry are
+   * always separated by slashes ('/').
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Sets the time of last modification of the entry.
+   *
+   * @time the time of last modification of the entry.
+   */
+  public void setTime(long time) {
+    Calendar cal = getCalendar();
+    synchronized (cal) {
+      cal.setTime(new Date(time * 1000L));
+      dostime = (cal.get(Calendar.YEAR) - 1980 & 0x7f) << 25
+              | (cal.get(Calendar.MONTH) + 1) << 21
+              | (cal.get(Calendar.DAY_OF_MONTH)) << 16
+              | (cal.get(Calendar.HOUR_OF_DAY)) << 11
+              | (cal.get(Calendar.MINUTE)) << 5
+              | (cal.get(Calendar.SECOND)) >> 1;
+    }
+    dostime = (int) (dostime / 1000L);
+    this.known |= KNOWN_TIME;
+  }
+
+  /**
+   * Gets the time of last modification of the entry.
+   *
+   * @return the time of last modification of the entry, or -1 if unknown.
+   */
+  public long getTime() {
+    if ((known & KNOWN_TIME) == 0)
+      return -1;
+
+    int sec = 2 * (dostime & 0x1f);
+    int min = (dostime >> 5) & 0x3f;
+    int hrs = (dostime >> 11) & 0x1f;
+    int day = (dostime >> 16) & 0x1f;
+    int mon = ((dostime >> 21) & 0xf) - 1;
+    int year = ((dostime >> 25) & 0x7f) + 1980; /* since 1900 */
+
+    try {
+      cal = getCalendar();
+      synchronized (cal) {
+        cal.set(year, mon, day, hrs, min, sec);
+        return cal.getTime().getTime();
+      }
+    } catch (RuntimeException ex) {
+      /* Ignore illegal time stamp */
+      known &= ~KNOWN_TIME;
+      return -1;
+    }
+  }
+
+  private static synchronized Calendar getCalendar() {
+    if (cal == null)
+      cal = Calendar.getInstance();
+
+    return cal;
+  }
+
+  /**
+   * Sets the size of the uncompressed data.
+   *
+   * @throws IllegalArgumentException if size is not in 0..0xffffffffL
+   */
+  public void setSize(long size) {
+    if ((size & 0xffffffff00000000L) != 0)
+      throw new IllegalArgumentException();
+    this.size = (int) size;
+    this.known |= KNOWN_SIZE;
+  }
+
+  /**
+   * Gets the size of the uncompressed data.
+   *
+   * @return the size or -1 if unknown.
+   */
+  public long getSize() {
+    return (known & KNOWN_SIZE) != 0 ? size & 0xffffffffL : -1L;
+  }
+
+  /**
+   * Sets the size of the compressed data.
+   *
+   * @throws IllegalArgumentException if size is not in 0..0xffffffffL
+   */
+  public void setCompressedSize(long csize) {
+    if ((csize & 0xffffffff00000000L) != 0)
+      throw new IllegalArgumentException();
+    this.compressedSize = (int) csize;
+    this.known |= KNOWN_CSIZE;
+  }
+
+  /**
+   * Gets the size of the compressed data.
+   *
+   * @return the size or -1 if unknown.
+   */
+  public long getCompressedSize() {
+    return (known & KNOWN_CSIZE) != 0 ? compressedSize & 0xffffffffL : -1L;
+  }
+
+  /**
+   * Sets the crc of the uncompressed data.
+   *
+   * @throws IllegalArgumentException if crc is not in 0..0xffffffffL
+   */
+  public void setCrc(long crc) {
+    if ((crc & 0xffffffff00000000L) != 0)
+      throw new IllegalArgumentException();
+    this.crc = (int) crc;
+    this.known |= KNOWN_CRC;
+  }
+
+  /**
+   * Gets the crc of the uncompressed data.
+   *
+   * @return the crc or -1 if unknown.
+   */
+  public long getCrc() {
+    return (known & KNOWN_CRC) != 0 ? crc & 0xffffffffL : -1L;
+  }
+
+  /**
+   * Sets the compression method.  Only DEFLATED and STORED are
+   * supported.
+   *
+   * @throws IllegalArgumentException if method is not supported.
+   * @see ZipOutputStream#DEFLATED
+   * @see ZipOutputStream#STORED
+   */
+  public void setMethod(int method) {
+    if (method != ZipOutputStream.STORED
+            && method != ZipOutputStream.DEFLATED)
+      throw new IllegalArgumentException();
+    this.method = (short) method;
+  }
+
+  /**
+   * Gets the compression method.
+   *
+   * @return the compression method or -1 if unknown.
+   */
+  public int getMethod() {
+    return method;
+  }
+
+  /**
+   * Sets the extra data.
+   *
+   * @throws IllegalArgumentException if extra is longer than 0xffff bytes.
+   */
+  public void setExtra(byte[] extra) {
+    if (extra == null) {
+      this.extra = null;
+      return;
+    }
+
+    if (extra.length > 0xffff)
+      throw new IllegalArgumentException();
+    this.extra = extra;
+    try {
+      int pos = 0;
+      while (pos < extra.length) {
+        int sig = (extra[pos++] & 0xff)
+                | (extra[pos++] & 0xff) << 8;
+        int len = (extra[pos++] & 0xff)
+                | (extra[pos++] & 0xff) << 8;
+        if (sig == 0x5455) {
+          /* extended time stamp */
+          int flags = extra[pos];
+          if ((flags & 1) != 0) {
+            long time = ((extra[pos + 1] & 0xff)
+                    | (extra[pos + 2] & 0xff) << 8
+                    | (extra[pos + 3] & 0xff) << 16
+                    | (extra[pos + 4] & 0xff) << 24);
+            setTime(time);
+          }
+        }
+        pos += len;
+      }
+    } catch (ArrayIndexOutOfBoundsException ex) {
+      /* be lenient */
+      return;
+    }
+  }
+
+  /**
+   * Gets the extra data.
+   *
+   * @return the extra data or null if not set.
+   */
+  public byte[] getExtra() {
+    return extra;
+  }
+
+  /**
+   * Sets the entry comment.
+   *
+   * @throws IllegalArgumentException if comment is longer than 0xffff.
+   */
+  public void setComment(String comment) {
+    if (comment != null && comment.length() > 0xffff)
+      throw new IllegalArgumentException();
+    this.comment = comment;
+  }
+
+  /**
+   * Gets the comment.
+   *
+   * @return the comment or null if not set.
+   */
+  public String getComment() {
+    return comment;
+  }
+
+  /**
+   * Gets true, if the entry is a directory.  This is solely
+   * determined by the name, a trailing slash '/' marks a directory.
+   */
+  public boolean isDirectory() {
+    int nlen = name.length();
+    return nlen > 0 && name.charAt(nlen - 1) == '/';
+  }
+
+  /**
+   * Gets the string representation of this AndroidZipEntry.  This is just
+   * the name as returned by getName().
+   */
+  public String toString() {
+    return name;
+  }
+
+  /**
+   * Gets the hashCode of this AndroidZipEntry.  This is just the hashCode
+   * of the name.  Note that the equals method isn't changed, though.
+   */
+  public int hashCode() {
+    return name.hashCode();
+  }
+}

+ 490 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/AndroidZipFile.java

@@ -0,0 +1,490 @@
+
+
+package me.ag2s.epublib.util.zip;
+
+import static me.ag2s.base.PfdHelper.seek;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+
+import java.io.BufferedInputStream;
+import java.io.DataInput;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipOutputStream;
+
+import me.ag2s.base.PfdHelper;
+
+/**
+ * This class represents a Zip archive.  You can ask for the contained
+ * entries, or get an input stream for a file entry.  The entry is
+ * automatically decompressed.
+ * <p>
+ * This class is thread safe:  You can open input streams for arbitrary
+ * entries in different threads.
+ *
+ * @author Jochen Hoenicke
+ * @author Artur Biesiadowski
+ */
+public class AndroidZipFile implements ZipConstants {
+
+    /**
+     * Mode flag to open a zip file for reading.
+     */
+    public static final int OPEN_READ = 0x1;
+
+    /**
+     * Mode flag to delete a zip file after reading.
+     */
+    public static final int OPEN_DELETE = 0x4;
+
+    // Name of this zip file.
+    private final String name;
+
+    // File from which zip entries are read.
+    //private final RandomAccessFile raf;
+
+    private final ParcelFileDescriptor pfd;
+
+    // The entries of this zip file when initialized and not yet closed.
+    private HashMap<String, AndroidZipEntry> entries;
+
+    private boolean closed = false;
+
+    /**
+     * Opens a Zip file with the given name for reading.
+     *
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the file doesn't contain a valid zip
+     *                      archive.
+     */
+    public AndroidZipFile(@NonNull ParcelFileDescriptor pfd, String name) throws ZipException, IOException {
+        this.pfd = pfd;
+        this.name = name;
+    }
+
+    /**
+     * Opens a Zip file reading the given File.
+     *
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the file doesn't contain a valid zip
+     *                      archive.
+     */
+    public AndroidZipFile(File file) throws ZipException, IOException {
+        this.pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+        this.name = file.getPath();
+    }
+
+    /**
+     * Opens a Zip file reading the given File in the given mode.
+     * <p>
+     * If the OPEN_DELETE mode is specified, the zip file will be deleted at
+     * some time moment after it is opened. It will be deleted before the zip
+     * file is closed or the Virtual Machine exits.
+     * <p>
+     * The contents of the zip file will be accessible until it is closed.
+     * <p>
+     * The OPEN_DELETE mode is currently unimplemented in this library
+     *
+     * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the file doesn't contain a valid zip
+     *                      archive.
+     * @since JDK1.3
+     */
+//    public AndroidZipFile(File file, int mode) throws ZipException, IOException {
+//        if ((mode & OPEN_DELETE) != 0) {
+//            throw new IllegalArgumentException
+//                    ("OPEN_DELETE mode not supported yet in net.sf.jazzlib.AndroidZipFile");
+//        }
+//        this.raf = new RandomAccessFile(file, "r");
+//        this.name = file.getPath();
+//    }
+
+    /**
+     * Read an unsigned short in little endian byte order from the given
+     * DataInput stream using the given byte buffer.
+     *
+     * @param di DataInput stream to read from.
+     * @param b  the byte buffer to read in (must be at least 2 bytes long).
+     * @return The value read.
+     * @throws IOException  if a i/o error occured.
+     * @throws EOFException if the file ends prematurely
+     */
+    private final int readLeShort(DataInput di, byte[] b) throws IOException {
+        di.readFully(b, 0, 2);
+        return (b[0] & 0xff) | (b[1] & 0xff) << 8;
+    }
+
+    private final int readLeShort(ParcelFileDescriptor pfd, byte[] b) throws IOException {
+        PfdHelper.readFully(pfd, b, 0, 2);//di.readFully(b, 0, 2);
+        return (b[0] & 0xff) | (b[1] & 0xff) << 8;
+    }
+
+    /**
+     * Read an int in little endian byte order from the given
+     * DataInput stream using the given byte buffer.
+     *
+     * @param di DataInput stream to read from.
+     * @param b  the byte buffer to read in (must be at least 4 bytes long).
+     * @return The value read.
+     * @throws IOException  if a i/o error occured.
+     * @throws EOFException if the file ends prematurely
+     */
+    private final int readLeInt(DataInput di, byte[] b) throws IOException {
+        di.readFully(b, 0, 4);
+        return ((b[0] & 0xff) | (b[1] & 0xff) << 8)
+                | ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16;
+    }
+
+    private final int readLeInt(ParcelFileDescriptor pfd, byte[] b) throws IOException {
+        PfdHelper.readFully(pfd, b, 0, 4);//di.readFully(b, 0, 4);
+        return ((b[0] & 0xff) | (b[1] & 0xff) << 8)
+                | ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16;
+    }
+
+
+    /**
+     * Read an unsigned short in little endian byte order from the given
+     * byte buffer at the given offset.
+     *
+     * @param b   the byte array to read from.
+     * @param off the offset to read from.
+     * @return The value read.
+     */
+    private final int readLeShort(byte[] b, int off) {
+        return (b[off] & 0xff) | (b[off + 1] & 0xff) << 8;
+    }
+
+    /**
+     * Read an int in little endian byte order from the given
+     * byte buffer at the given offset.
+     *
+     * @param b   the byte array to read from.
+     * @param off the offset to read from.
+     * @return The value read.
+     */
+    private final int readLeInt(byte[] b, int off) {
+        return ((b[off] & 0xff) | (b[off + 1] & 0xff) << 8)
+                | ((b[off + 2] & 0xff) | (b[off + 3] & 0xff) << 8) << 16;
+    }
+
+
+    /**
+     * Read the central directory of a zip file and fill the entries
+     * array.  This is called exactly once when first needed. It is called
+     * while holding the lock on <code>raf</code>.
+     *
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the central directory is malformed
+     */
+    private void readEntries() throws ZipException, IOException {
+        /* Search for the End Of Central Directory.  When a zip comment is
+         * present the directory may start earlier.
+         * FIXME: This searches the whole file in a very slow manner if the
+         * file isn't a zip file.
+         */
+        //long pos = raf.length() - ENDHDR;
+        long pos = PfdHelper.length(pfd) - ENDHDR;
+        byte[] ebs = new byte[CENHDR];
+
+        do {
+            if (pos < 0)
+                throw new ZipException
+                        ("central directory not found, probably not a zip file: " + name);
+            //raf.seek(pos--);
+            seek(pfd, pos--);
+        }
+        //while (readLeInt(raf, ebs) != ENDSIG);
+        while (readLeInt(pfd, ebs) != ENDSIG);
+
+        if (PfdHelper.skipBytes(pfd, ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
+            throw new EOFException(name);
+        //int count = readLeShort(raf, ebs);
+        int count = readLeShort(pfd, ebs);
+        if (PfdHelper.skipBytes(pfd, ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
+            throw new EOFException(name);
+        int centralOffset = readLeInt(pfd, ebs);
+
+        entries = new HashMap<>(count + count / 2);
+        //raf.seek(centralOffset);
+        seek(pfd, centralOffset);
+
+        byte[] buffer = new byte[16];
+        for (int i = 0; i < count; i++) {
+            //raf.readFully(ebs);
+            PfdHelper.readFully(pfd, ebs);
+            if (readLeInt(ebs, 0) != CENSIG)
+                throw new ZipException("Wrong Central Directory signature: " + name);
+
+            int method = readLeShort(ebs, CENHOW);
+            int dostime = readLeInt(ebs, CENTIM);
+            int crc = readLeInt(ebs, CENCRC);
+            int csize = readLeInt(ebs, CENSIZ);
+            int size = readLeInt(ebs, CENLEN);
+            int nameLen = readLeShort(ebs, CENNAM);
+            int extraLen = readLeShort(ebs, CENEXT);
+            int commentLen = readLeShort(ebs, CENCOM);
+
+            int offset = readLeInt(ebs, CENOFF);
+
+            int needBuffer = Math.max(nameLen, commentLen);
+            if (buffer.length < needBuffer)
+                buffer = new byte[needBuffer];
+
+            PfdHelper.readFully(pfd, buffer, 0, nameLen);
+            String name = new String(buffer, 0, 0, nameLen);
+
+            AndroidZipEntry entry = new AndroidZipEntry(name);
+            entry.setMethod(method);
+            entry.setCrc(crc & 0xffffffffL);
+            entry.setSize(size & 0xffffffffL);
+            entry.setCompressedSize(csize & 0xffffffffL);
+            entry.setTime(dostime);
+            if (extraLen > 0) {
+                byte[] extra = new byte[extraLen];
+                PfdHelper.readFully(pfd, extra);
+                entry.setExtra(extra);
+            }
+            if (commentLen > 0) {
+                PfdHelper.readFully(pfd, buffer, 0, commentLen);
+                entry.setComment(new String(buffer, 0, commentLen));
+            }
+            entry.offset = offset;
+            //ZipEntryHelper.setOffset(entry,offset);
+
+            //entry. = offset;
+            entries.put(name, entry);
+        }
+    }
+
+    /**
+     * Closes the AndroidZipFile.  This also closes all input streams given by
+     * this class.  After this is called, no further method should be
+     * called.
+     *
+     * @throws IOException if a i/o error occured.
+     */
+    public void close() throws IOException {
+        synchronized (pfd) {
+            closed = true;
+            entries = null;
+            pfd.close();
+        }
+    }
+
+    /**
+     * Calls the <code>close()</code> method when this AndroidZipFile has not yet
+     * been explicitly closed.
+     */
+    protected void finalize() throws IOException {
+        if (!closed && pfd != null) close();
+    }
+
+    /**
+     * Returns an enumeration of all Zip entries in this Zip file.
+     */
+    public Enumeration<AndroidZipEntry> entries() {
+        try {
+            return new ZipEntryEnumeration(getEntries().values().iterator());
+        } catch (IOException ioe) {
+            return null;
+        }
+    }
+
+    /**
+     * Checks that the AndroidZipFile is still open and reads entries when necessary.
+     *
+     * @throws IllegalStateException when the AndroidZipFile has already been closed.
+     * @throws java,                 IOEexception          when the entries could not be read.
+     */
+    private HashMap<String, AndroidZipEntry> getEntries() throws IOException {
+        synchronized (pfd) {
+            if (closed)
+                throw new IllegalStateException("AndroidZipFile has closed: " + name);
+
+            if (entries == null)
+                readEntries();
+
+            return entries;
+        }
+    }
+
+    /**
+     * Searches for a zip entry in this archive with the given name.
+     *
+     * @param name name. May contain directory components separated by
+     *             slashes ('/').
+     * @return the zip entry, or null if no entry with that name exists.
+     */
+    public AndroidZipEntry getEntry(String name) {
+        try {
+            HashMap<String, AndroidZipEntry> entries = getEntries();
+            AndroidZipEntry entry = entries.get(name);
+            return entry != null ? (AndroidZipEntry) entry.clone() : null;
+        } catch (IOException ioe) {
+            return null;
+        }
+    }
+
+
+    //access should be protected by synchronized(raf)
+    private final byte[] locBuf = new byte[LOCHDR];
+
+    /**
+     * Checks, if the local header of the entry at index i matches the
+     * central directory, and returns the offset to the data.
+     *
+     * @param entry to check.
+     * @return the start offset of the (compressed) data.
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the local header doesn't match the
+     *                      central directory header
+     */
+    private long checkLocalHeader(AndroidZipEntry entry) throws IOException {
+        synchronized (pfd) {
+            seek(pfd, entry.offset);
+            PfdHelper.readFully(pfd, locBuf);
+
+            if (readLeInt(locBuf, 0) != LOCSIG)
+                throw new ZipException("Wrong Local header signature: " + name);
+
+            if (entry.getMethod() != readLeShort(locBuf, LOCHOW))
+                throw new ZipException("Compression method mismatch: " + name);
+
+            if (entry.getName().length() != readLeShort(locBuf, LOCNAM))
+                throw new ZipException("file name length mismatch: " + name);
+
+            int extraLen = entry.getName().length() + readLeShort(locBuf, LOCEXT);
+            return entry.offset + LOCHDR + extraLen;
+        }
+    }
+
+    /**
+     * Creates an input stream reading the given zip entry as
+     * uncompressed data.  Normally zip entry should be an entry
+     * returned by getEntry() or entries().
+     *
+     * @param entry the entry to create an InputStream for.
+     * @return the input stream.
+     * @throws IOException  if a i/o error occured.
+     * @throws ZipException if the Zip archive is malformed.
+     */
+    public InputStream getInputStream(AndroidZipEntry entry) throws IOException {
+        HashMap<String, AndroidZipEntry> entries = getEntries();
+        String name = entry.getName();
+        AndroidZipEntry zipEntry = (AndroidZipEntry) entries.get(name);
+        if (zipEntry == null)
+            throw new NoSuchElementException(name);
+
+        long start = checkLocalHeader((AndroidZipEntry) zipEntry);
+        int method = zipEntry.getMethod();
+        InputStream is = new BufferedInputStream(new PartialInputStream
+                (pfd, start, zipEntry.getCompressedSize()));
+        switch (method) {
+            case ZipOutputStream.STORED:
+                return is;
+            case ZipOutputStream.DEFLATED:
+                return new InflaterInputStream(is, new Inflater(true));
+            default:
+                throw new ZipException("Unknown compression method " + method);
+        }
+    }
+
+    /**
+     * Returns the (path) name of this zip file.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns the number of entries in this zip file.
+     */
+    public int size() {
+        try {
+            return getEntries().size();
+        } catch (IOException ioe) {
+            return 0;
+        }
+    }
+
+    private static class ZipEntryEnumeration implements Enumeration<AndroidZipEntry> {
+        private final Iterator<AndroidZipEntry> elements;
+
+        public ZipEntryEnumeration(Iterator<AndroidZipEntry> elements) {
+            this.elements = elements;
+        }
+
+        public boolean hasMoreElements() {
+            return elements.hasNext();
+        }
+
+        public AndroidZipEntry nextElement() {
+            /* We return a clone, just to be safe that the user doesn't
+             * change the entry.
+             */
+            return (AndroidZipEntry) (elements.next()).clone();
+        }
+    }
+
+    private static class PartialInputStream extends InputStream {
+        private final ParcelFileDescriptor pfd;
+        long filepos, end;
+
+        public PartialInputStream(ParcelFileDescriptor pfd, long start, long len) {
+            this.pfd = pfd;
+            filepos = start;
+            end = start + len;
+        }
+
+        public int available() {
+            long amount = end - filepos;
+            if (amount > Integer.MAX_VALUE)
+                return Integer.MAX_VALUE;
+            return (int) amount;
+        }
+
+        public int read() throws IOException {
+            if (filepos == end)
+                return -1;
+            synchronized (pfd) {
+                seek(pfd, filepos++);
+                return PfdHelper.read(pfd);
+            }
+        }
+
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (len > end - filepos) {
+                len = (int) (end - filepos);
+                if (len == 0)
+                    return -1;
+            }
+            synchronized (pfd) {
+                seek(pfd, filepos);
+                int count = PfdHelper.read(pfd, b, off, len);
+                if (count > 0)
+                    filepos += len;
+                return count;
+            }
+        }
+
+        public long skip(long amount) {
+            if (amount < 0)
+                throw new IllegalArgumentException();
+            if (amount > end - filepos)
+                amount = end - filepos;
+            filepos += amount;
+            return amount;
+        }
+    }
+}

+ 61 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/ZipConstants.java

@@ -0,0 +1,61 @@
+
+
+package me.ag2s.epublib.util.zip;
+
+interface ZipConstants {
+  /* The local file header */
+  int LOCHDR = 30;
+  int LOCSIG = 'P' | ('K' << 8) | (3 << 16) | (4 << 24);
+
+  int LOCVER = 4;
+  int LOCFLG = 6;
+  int LOCHOW = 8;
+  int LOCTIM = 10;
+  int LOCCRC = 14;
+  int LOCSIZ = 18;
+  int LOCLEN = 22;
+  int LOCNAM = 26;
+  int LOCEXT = 28;
+
+  /* The Data descriptor */
+  int EXTSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24);
+  int EXTHDR = 16;
+
+  int EXTCRC = 4;
+  int EXTSIZ = 8;
+  int EXTLEN = 12;
+
+  /* The central directory file header */
+  int CENSIG = 'P' | ('K' << 8) | (1 << 16) | (2 << 24);
+  int CENHDR = 46;
+
+  int CENVEM = 4;
+  int CENVER = 6;
+  int CENFLG = 8;
+  int CENHOW = 10;
+  int CENTIM = 12;
+  int CENCRC = 16;
+  int CENSIZ = 20;
+  int CENLEN = 24;
+  int CENNAM = 28;
+  int CENEXT = 30;
+  int CENCOM = 32;
+  int CENDSK = 34;
+  int CENATT = 36;
+  int CENATX = 38;
+  int CENOFF = 42;
+
+  /* The entries in the end of central directory */
+  int ENDSIG = 'P' | ('K' << 8) | (5 << 16) | (6 << 24);
+  int ENDHDR = 22;
+
+  /* The following two fields are missing in SUN JDK */
+  int ENDNRD = 4;
+  int ENDDCD = 6;
+  int ENDSUB = 8;
+  int ENDTOT = 10;
+  int ENDSIZ = 12;
+  int ENDOFF = 16;
+  int ENDCOM = 20;
+}
+

+ 75 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/ZipEntryWrapper.java

@@ -0,0 +1,75 @@
+package me.ag2s.epublib.util.zip;
+
+import androidx.annotation.NonNull;
+
+import java.util.zip.ZipEntry;
+
+public class ZipEntryWrapper {
+    @NonNull
+    private final Object zipEntry;
+
+    public void checkType() {
+
+        if (zipEntry instanceof java.util.zip.ZipEntry || zipEntry instanceof AndroidZipEntry) {
+        } else {
+            throw new RuntimeException("使用了不支持的类");
+        }
+    }
+
+    public ZipEntryWrapper(@NonNull ZipEntry zipEntry) {
+        this.zipEntry = zipEntry;
+    }
+
+    public ZipEntryWrapper(@NonNull AndroidZipEntry zipEntry) {
+        this.zipEntry = zipEntry;
+    }
+
+    public ZipEntryWrapper(@NonNull Object element) {
+
+        this.zipEntry = element;
+        checkType();
+    }
+
+    public boolean isDirectory() {
+        checkType();
+        if (zipEntry instanceof ZipEntry) {
+            return ((ZipEntry) zipEntry).isDirectory();
+        }
+        if (zipEntry instanceof AndroidZipEntry) {
+            return ((AndroidZipEntry) zipEntry).isDirectory();
+        }
+        return true;
+    }
+
+    public ZipEntry getZipEntry() {
+        return (ZipEntry) zipEntry;
+    }
+
+    public AndroidZipEntry getAndroidZipEntry() {
+        return (AndroidZipEntry) zipEntry;
+    }
+
+    public String getName() {
+        checkType();
+        if (zipEntry instanceof ZipEntry) {
+            return ((ZipEntry) zipEntry).getName();
+        }
+        if (zipEntry instanceof AndroidZipEntry) {
+            return ((AndroidZipEntry) zipEntry).getName();
+        }
+        return null;
+    }
+
+    public long getSize() {
+        checkType();
+        if (zipEntry instanceof ZipEntry) {
+            return ((ZipEntry) zipEntry).getSize();
+        }
+        if (zipEntry instanceof AndroidZipEntry) {
+            return ((AndroidZipEntry) zipEntry).getSize();
+        }
+        return -1;
+    }
+
+
+}

+ 34 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/ZipException.java

@@ -0,0 +1,34 @@
+
+
+package me.ag2s.epublib.util.zip;
+
+import java.io.IOException;
+
+/**
+ * Thrown during the creation or input of a zip file.
+ *
+ * @author Jochen Hoenicke
+ * @author Per Bothner
+ * @status updated to 1.4
+ */
+public class ZipException extends IOException {
+    /**
+     * Compatible with JDK 1.0+.
+     */
+    private static final long serialVersionUID = 8000196834066748623L;
+
+    /**
+     * Create an exception without a message.
+     */
+    public ZipException() {
+    }
+
+    /**
+     * Create an exception with a message.
+     *
+     * @param msg the message
+     */
+    public ZipException(String msg) {
+        super(msg);
+    }
+}

+ 103 - 0
epublib/src/main/java/me/ag2s/epublib/util/zip/ZipFileWrapper.java

@@ -0,0 +1,103 @@
+package me.ag2s.epublib.util.zip;
+
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.zip.ZipFile;
+
+/**
+ * 对ZipFile的包装
+ */
+
+public class ZipFileWrapper {
+    @NonNull
+    private final Object zipFile;
+
+
+    public void checkType() {
+        if (zipFile instanceof java.util.zip.ZipFile || zipFile instanceof AndroidZipFile) {
+        } else {
+            throw new RuntimeException("使用了不支持的类");
+        }
+    }
+
+    public ZipFileWrapper(@NonNull ZipFile zipFile) {
+        this.zipFile = zipFile;
+        checkType();
+    }
+
+    public ZipFileWrapper(@NonNull AndroidZipFile zipFile) {
+        this.zipFile = zipFile;
+        checkType();
+    }
+
+    public String getName() {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            return ((ZipFile) zipFile).getName();
+        } else if (zipFile instanceof AndroidZipFile) {
+            return ((AndroidZipFile) zipFile).getName();
+        } else {
+            return null;
+        }
+    }
+
+    public String getComment() {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            return ((ZipFile) zipFile).getComment();
+        } else if (zipFile instanceof AndroidZipFile) {
+            return ((AndroidZipFile) zipFile).getName();
+        } else {
+            return null;
+        }
+    }
+
+    public ZipEntryWrapper getEntry(String name) {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            return new ZipEntryWrapper(((ZipFile) zipFile).getEntry(name));
+        } else if (zipFile instanceof AndroidZipFile) {
+            return new ZipEntryWrapper(((AndroidZipFile) zipFile).getEntry(name));
+        } else {
+            return null;
+        }
+    }
+
+    public Enumeration entries() {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            return ((ZipFile) zipFile).entries();
+
+        } else if (zipFile instanceof AndroidZipFile) {
+            return ((AndroidZipFile) zipFile).entries();
+        } else {
+            return null;
+        }
+    }
+
+    public InputStream getInputStream(ZipEntryWrapper entry) throws IOException {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            return ((ZipFile) zipFile).getInputStream(entry.getZipEntry());
+        } else if (zipFile instanceof AndroidZipFile) {
+            return ((AndroidZipFile) zipFile).getInputStream(entry.getAndroidZipEntry());
+        } else {
+            return null;
+        }
+    }
+
+    public void close() throws IOException {
+        checkType();
+        if (zipFile instanceof java.util.zip.ZipFile) {
+            ((ZipFile) zipFile).close();
+        } else if (zipFile instanceof AndroidZipFile) {
+            ((AndroidZipFile) zipFile).close();
+        }
+    }
+
+
+}