diff --git a/app/src/main/java/io/legado/app/help/http/cronet/AbsCallBack.kt b/app/src/main/java/io/legado/app/help/http/cronet/AbsCallBack.kt index cfa55a2b1..56f5b4bfd 100644 --- a/app/src/main/java/io/legado/app/help/http/cronet/AbsCallBack.kt +++ b/app/src/main/java/io/legado/app/help/http/cronet/AbsCallBack.kt @@ -2,6 +2,7 @@ package io.legado.app.help.http.cronet import io.legado.app.help.http.okHttpClient import io.legado.app.utils.DebugLog +import io.legado.app.utils.rethrowAsIOException import okhttp3.* import okhttp3.EventListener import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -123,7 +124,7 @@ abstract class AbsCallBack( //UrlResponseInfo可能为null override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) { DebugLog.i(javaClass.name, error.message.toString()) - mException = IOException(error.message, error) + mException = error.rethrowAsIOException() this.eventListener?.callFailed(mCall, error) responseCallback?.onFailure(mCall, error) } diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt index 1ed21f167..a5604bfe3 100644 --- a/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt @@ -55,9 +55,12 @@ object CronetLoader : CronetEngine.Builder.LibraryLoader() { * 判断Cronet是否安装完成 */ fun install(): Boolean { - if (cacheInstall) { - return true + synchronized(this) { + if (cacheInstall) { + return true + } } + if (AppConfig.isGooglePlay) { return false } diff --git a/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt b/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt index 409152490..a5bfaa8fc 100644 --- a/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt @@ -1,5 +1,7 @@ package io.legado.app.utils +import java.io.IOException + val Throwable.msg: String get() { val stackTrace = stackTraceToString() @@ -8,4 +10,10 @@ val Throwable.msg: String stackTrace.isNotEmpty() -> stackTrace else -> lMsg } - } \ No newline at end of file + } + +fun Throwable.rethrowAsIOException(): IOException { + val newException = IOException(this.message) + newException.initCause(this) + throw newException +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java b/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java index 1e476191c..7401b00f9 100644 --- a/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java +++ b/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java @@ -1,5 +1,7 @@ package me.ag2s.epublib.zip; +import static me.ag2s.utils.ThrowableUtils.rethrowAsIOException; + import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -16,78 +18,156 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UTFDataFormatException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; +import me.ag2s.utils.AndroidCloseGuard; + +@SuppressWarnings("unused") public class AndroidRandomReadableFile implements DataInput, Closeable { - private ParcelFileDescriptor pfd; + private final ParcelFileDescriptor pfd; private FileInputStream fis; - private DataInputStream dis; - public long pos = 0; + private final Object lock = new Object(); + + /** + * 读取基本类型的buffer + */ + private final byte[] readBuffer = new byte[8]; + private long pos = 0; + + private final AndroidCloseGuard guard = AndroidCloseGuard.getInstance(); public AndroidRandomReadableFile(@NonNull Context context, @NonNull Uri treeUri) throws FileNotFoundException { - try { - pfd = context.getContentResolver().openFileDescriptor(treeUri, "r"); - fis = new FileInputStream(pfd.getFileDescriptor()); - dis = new DataInputStream(fis); - } catch (FileNotFoundException e) { - throw e; - } + pfd = context.getContentResolver().openFileDescriptor(treeUri, "r"); + fis = new FileInputStream(pfd.getFileDescriptor()); + guard.open("close"); + } + public final FileDescriptor getFD() { return pfd.getFileDescriptor(); } + + /** + * Returns the unique {@link java.nio.channels.FileChannel FileChannel} + * object associated with this file. + * + *

The {@link java.nio.channels.FileChannel#position() + * position} of the returned channel will always be equal to + * this object's file-pointer offset as returned by the {@link + * #getFilePointer getFilePointer} method. Changing this object's + * file-pointer offset, whether explicitly or by reading or writing bytes, + * will change the position of the channel, and vice versa. Changing the + * file's length via this object will change the length seen via the file + * channel, and vice versa. + * + * @return the file channel associated with this file + * @spec JSR-51 + * @since 1.4 + */ public final FileChannel getChannel() { - if (fis == null || pos != getPos()) { - fis = new FileInputStream(pfd.getFileDescriptor()); + synchronized (lock) { + if (fis == null || pos != getPos()) { + fis = new FileInputStream(pfd.getFileDescriptor()); + } } + return fis.getChannel(); } public final FileInputStream getFileInputStream() { - if (fis == null || pos != getPos()) { - fis = new FileInputStream(pfd.getFileDescriptor()); + synchronized (lock) { + if (fis == null || this.pos != getPos()) { + this.pos = getPos(); + fis = new FileInputStream(pfd.getFileDescriptor()); + } } + return fis; } + /** + * Reads a byte of data from this input stream. This method blocks + * if no input is yet available. + * + * @return the next byte of data, or -1 if the end of the + * file is reached. + * @throws IOException if an I/O error occurs. + */ public int read() throws IOException { - byte[] b = new byte[1]; - return (read(b, 0, 1) != -1) ? b[0] & 0xff : -1; + return (read(readBuffer, 0, 1) != -1) ? readBuffer[0] & 0xff : -1; } - public int read(byte[] b) { + /** + * Reads a sub array as a sequence of bytes. + * + * @param b the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the number of bytes to read. + * @throws IOException If an I/O error has occurred. + */ + private int readBytes(byte[] b, int off, int len) throws IOException { try { - return read(b, 0, b.length); - } catch (IOException e) { - e.printStackTrace(); - return -1; + return android.system.Os.read(pfd.getFileDescriptor(), b, off, len); + } catch (ErrnoException e) { + throw rethrowAsIOException(e); } } + + /** + * Reads up to b.length bytes of data from this input + * stream into an array of bytes. This method blocks until some input + * is available. + * + * @param b the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the file has been reached. + * @throws IOException if an I/O error occurs. + */ + public int read(byte[] b) throws IOException { + return readBytes(b, 0, b.length); + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. If len is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and 0 is returned. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the file has been reached. + * @throws NullPointerException If b is null. + * @throws IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + * @throws IOException if an I/O error occurs. + */ public int read(byte[] b, int off, int len) throws IOException { - try { - return android.system.Os.read(pfd.getFileDescriptor(), b, off, len); - } catch (Exception e) { - return -1; - } + return readBytes(b, off, len); } private void syncInputStream() { + this.pos = getPos(); fis = new FileInputStream(pfd.getFileDescriptor()); - dis = new DataInputStream(fis); } public void seek(long pos) throws IOException { try { android.system.Os.lseek(pfd.getFileDescriptor(), pos, OsConstants.SEEK_SET); - } catch (Exception e) { - throw new IOException(e); + } catch (ErrnoException e) { + throw rethrowAsIOException(e); } } @@ -95,53 +175,199 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { public long length() throws IOException { try { return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_END); - } catch (Exception e) { - throw new IOException(e); + } catch (ErrnoException e) { + throw rethrowAsIOException(e); } } - public long getPos() { + /** + * Returns the current offset in this file. + * + * @return the offset from the beginning of the file, in bytes, + * at which the next read or write occurs. + * @throws IOException if an I/O error occurs. + */ + public long getFilePointer() throws IOException { try { return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_CUR); - } catch (Exception e) { - return -1; + } catch (ErrnoException e) { + throw rethrowAsIOException(e); } - } - @Override - public void readFully(byte[] b) throws IOException { + public long getPos() { try { - android.system.Os.read(pfd.getFileDescriptor(), b, 0, b.length); + return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_CUR); } catch (ErrnoException e) { - e.printStackTrace(); + return -1; } + } + + /** + * Reads some bytes from an input + * stream and stores them into the buffer + * array {@code b}. The number of bytes + * read is equal + * to the length of {@code b}. + *

+ * This method blocks until one of the + * following conditions occurs: + *

+ *

+ * If {@code b} is {@code null}, + * a {@code NullPointerException} is thrown. + * If {@code b.length} is zero, then + * no bytes are read. Otherwise, the first + * byte read is stored into element {@code b[0]}, + * the next one into {@code b[1]}, and + * so on. + * If an exception is thrown from + * this method, then it may be that some but + * not all bytes of {@code b} have been + * updated with data from the input stream. + * + * @param b the buffer into which the data is read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ + @Override + public void readFully(byte[] b) throws IOException { + readFully(b, 0, b.length); } + /** + * Reads {@code len} + * bytes from + * an input stream. + *

+ * This method + * blocks until one of the following conditions + * occurs: + *

+ *

+ * If {@code b} is {@code null}, + * a {@code NullPointerException} is thrown. + * If {@code off} is negative, or {@code len} + * is negative, or {@code off+len} is + * greater than the length of the array {@code b}, + * then an {@code IndexOutOfBoundsException} + * is thrown. + * If {@code len} is zero, + * then no bytes are read. Otherwise, the first + * byte read is stored into element {@code b[off]}, + * the next one into {@code b[off+1]}, + * and so on. The number of bytes read is, + * at most, equal to {@code len}. + * + * @param b the buffer into which the data is read. + * @param off an int specifying the offset into the data. + * @param len an int specifying the number of bytes to read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public void readFully(byte[] b, int off, int len) throws IOException { - try { - android.system.Os.read(pfd.getFileDescriptor(), b, off, len); - //syncInputStream(); - } catch (ErrnoException e) { - throw new IOException(e); - } + int n = 0; + do { + int count = this.read(b, off + n, len - n); + if (count < 0) + throw new EOFException(); + n += count; + } while (n < len); } + /** + * Makes an attempt to skip over + * {@code n} bytes + * of data from the input + * stream, discarding the skipped bytes. However, + * it may skip + * over some smaller number of + * bytes, possibly zero. This may result from + * any of a + * number of conditions; reaching + * end of file before {@code n} bytes + * have been skipped is + * only one possibility. + * This method never throws an {@code EOFException}. + * The actual + * number of bytes skipped is returned. + * + * @param n the number of bytes to be skipped. + * @return the number of bytes actually skipped. + * @throws IOException if an I/O error occurs. + */ @Override public int skipBytes(int n) throws IOException { - try { - byte[] b = new byte[n]; - return android.system.Os.read(pfd.getFileDescriptor(), b, 0, b.length); - } catch (Exception e) { - return -1; + long pos; + long len; + long newpos; + + if (n <= 0) { + return 0; + } + pos = getFilePointer(); + len = length(); + newpos = pos + n; + if (newpos > len) { + newpos = len; } + seek(newpos); + + /* return the actual number of bytes skipped */ + return (int) (newpos - pos); +// try { +// byte[] b = new byte[n]; +// return android.system.Os.read(pfd.getFileDescriptor(), b, 0, b.length); +// } catch (Exception e) { +// return -1; +// } } + + /** + * Reads one input byte and returns + * {@code true} if that byte is nonzero, + * {@code false} if that byte is zero. + * This method is suitable for reading + * the byte written by the {@code writeBoolean} + * method of interface {@code DataOutput}. + * + * @return the {@code boolean} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public boolean readBoolean() throws IOException { int ch = this.read(); @@ -151,6 +377,20 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } + /** + * Reads and returns one input byte. + * The byte is treated as a signed value in + * the range {@code -128} through {@code 127}, + * inclusive. + * This method is suitable for + * reading the byte written by the {@code writeByte} + * method of interface {@code DataOutput}. + * + * @return the 8-bit value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public byte readByte() throws IOException { int ch = this.read(); @@ -160,6 +400,24 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } + /** + * Reads one input byte, zero-extends + * it to type {@code int}, and returns + * the result, which is therefore in the range + * {@code 0} + * through {@code 255}. + * This method is suitable for reading + * the byte written by the {@code writeByte} + * method of interface {@code DataOutput} + * if the argument to {@code writeByte} + * was intended to be a value in the range + * {@code 0} through {@code 255}. + * + * @return the unsigned 8-bit value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public int readUnsignedByte() throws IOException { int ch = this.read(); @@ -168,32 +426,132 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { return ch; } - private final byte[] readBuffer = new byte[8]; + /** + * Reads two input bytes and returns + * a {@code short} value. Let {@code a} + * be the first byte read and {@code b} + * be the second byte. The value + * returned + * is: + *

{@code (short)((a << 8) | (b & 0xff))
+     * }
+ * This method + * is suitable for reading the bytes written + * by the {@code writeShort} method of + * interface {@code DataOutput}. + * + * @return the 16-bit value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public short readShort() throws IOException { readFully(readBuffer, 0, 2); return ByteBuffer.wrap(readBuffer).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(); } + /** + * Reads two input bytes and returns + * an {@code int} value in the range {@code 0} + * through {@code 65535}. Let {@code a} + * be the first byte read and + * {@code b} + * be the second byte. The value returned is: + *
{@code (((a & 0xff) << 8) | (b & 0xff))
+     * }
+ * This method is suitable for reading the bytes + * written by the {@code writeShort} method + * of interface {@code DataOutput} if + * the argument to {@code writeShort} + * was intended to be a value in the range + * {@code 0} through {@code 65535}. + * + * @return the unsigned 16-bit value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public int readUnsignedShort() throws IOException { readFully(readBuffer, 0, 2); return ByteBuffer.wrap(readBuffer).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get() & 0xffff; } + /** + * Reads two input bytes and returns a {@code char} value. + * Let {@code a} + * be the first byte read and {@code b} + * be the second byte. The value + * returned is: + *
{@code (char)((a << 8) | (b & 0xff))
+     * }
+ * This method + * is suitable for reading bytes written by + * the {@code writeChar} method of interface + * {@code DataOutput}. + * + * @return the {@code char} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public char readChar() throws IOException { readFully(readBuffer, 0, 2); - return (char) ByteBuffer.wrap(readBuffer).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(); + return ByteBuffer.wrap(readBuffer).order(ByteOrder.BIG_ENDIAN).asCharBuffer().get(); } + /** + * Reads four input bytes and returns an + * {@code int} value. Let {@code a-d} + * be the first through fourth bytes read. The value returned is: + *
{@code
+     * (((a & 0xff) << 24) | ((b & 0xff) << 16) |
+     *  ((c & 0xff) <<  8) | (d & 0xff))
+     * }
+ * This method is suitable + * for reading bytes written by the {@code writeInt} + * method of interface {@code DataOutput}. + * + * @return the {@code int} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public int readInt() throws IOException { readFully(readBuffer, 0, 4); return ByteBuffer.wrap(readBuffer).order(ByteOrder.BIG_ENDIAN).asIntBuffer().get(); } + + /** + * Reads eight input bytes and returns + * a {@code long} value. Let {@code a-h} + * be the first through eighth bytes read. + * The value returned is: + *
{@code
+     * (((long)(a & 0xff) << 56) |
+     *  ((long)(b & 0xff) << 48) |
+     *  ((long)(c & 0xff) << 40) |
+     *  ((long)(d & 0xff) << 32) |
+     *  ((long)(e & 0xff) << 24) |
+     *  ((long)(f & 0xff) << 16) |
+     *  ((long)(g & 0xff) <<  8) |
+     *  ((long)(h & 0xff)))
+     * }
+ *

+ * This method is suitable + * for reading bytes written by the {@code writeLong} + * method of interface {@code DataOutput}. + * + * @return the {@code long} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public long readLong() throws IOException { readFully(readBuffer, 0, 8); @@ -207,162 +565,167 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { ((readBuffer[7] & 255) << 0)); } + /** + * Reads four input bytes and returns + * a {@code float} value. It does this + * by first constructing an {@code int} + * value in exactly the manner + * of the {@code readInt} + * method, then converting this {@code int} + * value to a {@code float} in + * exactly the manner of the method {@code Float.intBitsToFloat}. + * This method is suitable for reading + * bytes written by the {@code writeFloat} + * method of interface {@code DataOutput}. + * + * @return the {@code float} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public float readFloat() throws IOException { return Float.intBitsToFloat(readInt()); } + /** + * Reads eight input bytes and returns + * a {@code double} value. It does this + * by first constructing a {@code long} + * value in exactly the manner + * of the {@code readLong} + * method, then converting this {@code long} + * value to a {@code double} in exactly + * the manner of the method {@code Double.longBitsToDouble}. + * This method is suitable for reading + * bytes written by the {@code writeDouble} + * method of interface {@code DataOutput}. + * + * @return the {@code double} value read. + * @throws EOFException if this stream reaches the end before reading + * all the bytes. + * @throws IOException if an I/O error occurs. + */ @Override public double readDouble() throws IOException { return Double.longBitsToDouble(readLong()); } - private char[] lineBuffer; - @Deprecated + /** + * Reads the next line of text from this file. This method successively + * reads bytes from the file, starting at the current file pointer, + * until it reaches a line terminator or the end + * of the file. Each byte is converted into a character by taking the + * byte's value for the lower eight bits of the character and setting the + * high eight bits of the character to zero. This method does not, + * therefore, support the full Unicode character set. + * + *

A line of text is terminated by a carriage-return character + * ({@code '\u005Cr'}), a newline character ({@code '\u005Cn'}), a + * carriage-return character immediately followed by a newline character, + * or the end of the file. Line-terminating characters are discarded and + * are not included as part of the string returned. + * + *

This method blocks until a newline character is read, a carriage + * return and the byte following it are read (to see if it is a newline), + * the end of the file is reached, or an exception is thrown. + * + * @return the next line of text from this file, or null if end + * of file is encountered before even one byte is read. + * @throws IOException if an I/O error occurs. + */ + @Override public String readLine() throws IOException { - char[] buf = lineBuffer; - - if (buf == null) { - buf = lineBuffer = new char[128]; - } - - int room = buf.length; - int offset = 0; - int c; + StringBuilder input = new StringBuilder(); + int c = -1; + boolean eol = false; - loop: - while (true) { - switch (c = this.read()) { + while (!eol) { + switch (c = read()) { case -1: case '\n': - break loop; - + eol = true; + break; case '\r': - int c2 = this.read(); - break loop; - - default: - if (--room < 0) { - buf = new char[offset + 128]; - room = buf.length - offset - 1; - System.arraycopy(lineBuffer, 0, buf, 0, offset); - lineBuffer = buf; + eol = true; + long cur = getFilePointer(); + if ((read()) != '\n') { + seek(cur); } - buf[offset++] = (char) c; + break; + default: + input.append((char) c); break; } } - if ((c == -1) && (offset == 0)) { + + if ((c == -1) && (input.length() == 0)) { return null; } - return String.copyValueOf(buf, 0, offset); + return input.toString(); } + /** + * See the general contract of the readUTF + * method of DataInput. + *

+ * Bytes + * for this operation are read from the contained + * input stream. + * + * @return a Unicode string. + * @throws EOFException if this input stream reaches the end before + * reading all the bytes. + * @throws IOException the stream has been closed and the contained + * input stream does not support reading after close, or + * another I/O error occurs. + * @throws UTFDataFormatException if the bytes do not represent a valid + * modified UTF-8 encoding of a string. + * @see java.io.DataInputStream#readUTF(java.io.DataInput) + */ @Override public String readUTF() throws IOException { - if (pos != getPos()) { - syncInputStream(); - } - return dis.readUTF(); + return DataInputStream.readUTF(this); } -// public final static String readUTF(DataInput in) throws IOException { -// int utflen = in.readUnsignedShort(); -// byte[] bytearr = null; -// char[] chararr = null; -// if (in instanceof DataInputStream) { -// DataInputStream dis = (DataInputStream)in; -// if (dis.bytearr.length < utflen){ -// dis.bytearr = new byte[utflen*2]; -// dis.chararr = new char[utflen*2]; -// } -// chararr = dis.chararr; -// bytearr = dis.bytearr; -// } else { -// bytearr = new byte[utflen]; -// chararr = new char[utflen]; -// } -// -// int c, char2, char3; -// int count = 0; -// int chararr_count=0; -// -// in.readFully(bytearr, 0, utflen); -// -// while (count < utflen) { -// c = (int) bytearr[count] & 0xff; -// if (c > 127) break; -// count++; -// chararr[chararr_count++]=(char)c; -// } -// -// while (count < utflen) { -// c = (int) bytearr[count] & 0xff; -// switch (c >> 4) { -// case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: -// /* 0xxxxxxx*/ -// count++; -// chararr[chararr_count++]=(char)c; -// break; -// case 12: case 13: -// /* 110x xxxx 10xx xxxx*/ -// count += 2; -// if (count > utflen) -// throw new UTFDataFormatException( -// "malformed input: partial character at end"); -// char2 = (int) bytearr[count-1]; -// if ((char2 & 0xC0) != 0x80) -// throw new UTFDataFormatException( -// "malformed input around byte " + count); -// chararr[chararr_count++]=(char)(((c & 0x1F) << 6) | -// (char2 & 0x3F)); -// break; -// case 14: -// /* 1110 xxxx 10xx xxxx 10xx xxxx */ -// count += 3; -// if (count > utflen) -// throw new UTFDataFormatException( -// "malformed input: partial character at end"); -// char2 = (int) bytearr[count-2]; -// char3 = (int) bytearr[count-1]; -// if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80)) -// throw new UTFDataFormatException( -// "malformed input around byte " + (count-1)); -// chararr[chararr_count++]=(char)(((c & 0x0F) << 12) | -// ((char2 & 0x3F) << 6) | -// ((char3 & 0x3F) << 0)); -// break; -// default: -// /* 10xx xxxx, 1111 xxxx */ -// throw new UTFDataFormatException( -// "malformed input around byte " + count); -// } -// } -// // The number of chars produced may be less than utflen -// return new String(chararr, 0, chararr_count); -// } @Override public void close() throws IOException { - try { - dis.close(); - } catch (Exception e) { - e.printStackTrace(); - } + + guard.close(); try { - fis.close(); + if (fis != null) { + fis.close(); + } } catch (Exception e) { e.printStackTrace(); } try { - pfd.close(); + if (pfd != null) { + pfd.close(); + } + } catch (Exception e) { e.printStackTrace(); } } + + protected void finalize() throws Throwable { + try { + // Note that guard could be null if the constructor threw. + + guard.warnIfOpen(); + + + close(); + + } finally { + super.finalize(); + } + } } diff --git a/epublib/src/main/java/me/ag2s/utils/Android11CloseGuard.java b/epublib/src/main/java/me/ag2s/utils/Android11CloseGuard.java new file mode 100644 index 000000000..a318315ca --- /dev/null +++ b/epublib/src/main/java/me/ag2s/utils/Android11CloseGuard.java @@ -0,0 +1,33 @@ +package me.ag2s.utils; + +import android.os.Build; +import android.util.CloseGuard; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +@RequiresApi(api = Build.VERSION_CODES.R) +final class Android11CloseGuard implements AndroidCloseGuard { + @NonNull + private final CloseGuard mImpl; + + + public Android11CloseGuard() { + mImpl = new CloseGuard(); + } + + @Override + public void open(@NonNull String closeMethodName) { + mImpl.open(closeMethodName); + } + + @Override + public void close() { + mImpl.close(); + } + + @Override + public void warnIfOpen() { + mImpl.warnIfOpen(); + } +} diff --git a/epublib/src/main/java/me/ag2s/utils/AndroidCloseGuard.java b/epublib/src/main/java/me/ag2s/utils/AndroidCloseGuard.java new file mode 100644 index 000000000..0472bfcc6 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/utils/AndroidCloseGuard.java @@ -0,0 +1,38 @@ +package me.ag2s.utils; + + +import android.os.Build; + +import androidx.annotation.NonNull; + +public interface AndroidCloseGuard { + + + static AndroidCloseGuard getInstance() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new Android11CloseGuard(); + } else { + return new AndroidRefCloseGuard(); + } + } + + /** + * Initializes the instance with a warning that the caller should have explicitly called the + * {@code closeMethodName} method instead of relying on finalization. + * + * @param closeMethodName non-null name of explicit termination method. Printed by warnIfOpen. + * @throws NullPointerException if closeMethodName is null. + */ + void open(@NonNull String closeMethodName); + + /** + * Marks this CloseGuard instance as closed to avoid warnings on finalization. + */ + void close(); + + /** + * Logs a warning if the caller did not properly cleanup by calling an explicit close method + * before finalization. + */ + void warnIfOpen(); +} diff --git a/epublib/src/main/java/me/ag2s/utils/AndroidRefCloseGuard.java b/epublib/src/main/java/me/ag2s/utils/AndroidRefCloseGuard.java new file mode 100644 index 000000000..deda4f3e4 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/utils/AndroidRefCloseGuard.java @@ -0,0 +1,83 @@ +package me.ag2s.utils; + +import androidx.annotation.NonNull; + +import java.lang.reflect.Method; + +final class AndroidRefCloseGuard implements AndroidCloseGuard { + private static Object closeGuardInstance; + private static Method getMethod; + private static Method closeMethod; + private static Method openMethod; + private static Method warnIfOpenMethod; + + + public AndroidRefCloseGuard() { + + if (getMethod == null || closeMethod == null || openMethod == null || warnIfOpenMethod == null) { + try { + Class closeGuardClass = Class.forName("dalvik.system.CloseGuard"); + getMethod = closeGuardClass.getMethod("get"); + closeMethod = closeGuardClass.getMethod("close"); + openMethod = closeGuardClass.getMethod("open", String.class); + warnIfOpenMethod = closeGuardClass.getMethod("warnIfOpen"); + } catch (Exception ignored) { + getMethod = null; + openMethod = null; + warnIfOpenMethod = null; + } + } + + + } + + Object createAndOpen(String closer) { + if (getMethod != null) { + try { + if (closeGuardInstance == null) { + closeGuardInstance = getMethod.invoke(null); + } + + openMethod.invoke(closeGuardInstance, closer); + return closeGuardInstance; + } catch (Exception ignored) { + } + } + return null; + } + + + boolean warnIfOpen(Object closeGuardInstance) { + boolean reported = false; + if (closeGuardInstance != null) { + try { + warnIfOpenMethod.invoke(closeGuardInstance); + reported = true; + } catch (Exception ignored) { + } + } + return reported; + } + + + @Override + public void open(@NonNull String closeMethodName) { + closeGuardInstance = createAndOpen(closeMethodName); + } + + @Override + public void close() { + if (closeGuardInstance != null) { + try { + closeMethod.invoke(closeMethod); + } catch (Exception ignored) { + } + } + + } + + @Override + public void warnIfOpen() { + warnIfOpen(closeGuardInstance); + } +} diff --git a/epublib/src/main/java/me/ag2s/utils/ThrowableUtils.java b/epublib/src/main/java/me/ag2s/utils/ThrowableUtils.java new file mode 100644 index 000000000..a5e7b1685 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/utils/ThrowableUtils.java @@ -0,0 +1,16 @@ +package me.ag2s.utils; + +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; + } +}