diff --git a/epublib/src/main/java/me/ag2s/epublib/util/Android11CloseGuard.java b/epublib/src/main/java/me/ag2s/epublib/util/Android11CloseGuard.java new file mode 100644 index 000000000..8e2d0ca50 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/Android11CloseGuard.java @@ -0,0 +1,33 @@ +package me.ag2s.epublib.util; + +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/epublib/util/AndroidCloseGuard.java b/epublib/src/main/java/me/ag2s/epublib/util/AndroidCloseGuard.java new file mode 100644 index 000000000..ba8b963e3 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/AndroidCloseGuard.java @@ -0,0 +1,38 @@ +package me.ag2s.epublib.util; + + +import android.os.Build; + +import androidx.annotation.NonNull; + +public interface AndroidCloseGuard { + + + public 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/epublib/util/AndroidRefCloseGuard.java b/epublib/src/main/java/me/ag2s/epublib/util/AndroidRefCloseGuard.java new file mode 100644 index 000000000..8daee7837 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/AndroidRefCloseGuard.java @@ -0,0 +1,83 @@ +package me.ag2s.epublib.util; + +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/epublib/zip/AndroidRandomReadableFile.java b/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java index 1e476191c..6fd7e668e 100644 --- a/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java +++ b/epublib/src/main/java/me/ag2s/epublib/zip/AndroidRandomReadableFile.java @@ -2,6 +2,7 @@ package me.ag2s.epublib.zip; import android.content.Context; import android.net.Uri; +import android.os.Build; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.OsConstants; @@ -16,26 +17,30 @@ 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.epublib.util.AndroidCloseGuard; + + public class AndroidRandomReadableFile implements DataInput, Closeable { - private ParcelFileDescriptor pfd; + private final ParcelFileDescriptor pfd; private FileInputStream fis; - private DataInputStream dis; - public long pos = 0; + 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()); + //dis = new DataInputStream(fis); + guard.open("close"); + } + public final FileDescriptor getFD() { return pfd.getFileDescriptor(); } @@ -48,38 +53,73 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } public final FileInputStream getFileInputStream() { - if (fis == null || pos != getPos()) { + 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; } - public int read(byte[] b) { - try { - return read(b, 0, b.length); - } catch (IOException e) { - e.printStackTrace(); - return -1; - } + + /** + * 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 read(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; + throw new IOException(e); } } private void syncInputStream() { + this.pos = getPos(); fis = new FileInputStream(pfd.getFileDescriptor()); - dis = new DataInputStream(fis); } @@ -110,6 +150,46 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } + /** + * 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 { try { @@ -121,6 +201,49 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } + /** + * 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 { @@ -131,6 +254,27 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } } + /** + * 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 { @@ -142,6 +286,19 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { } + /** + * 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 +308,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 +331,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(); @@ -170,30 +359,131 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { 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(); } + /** + * 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,11 +497,47 @@ 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()); @@ -219,6 +545,32 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { private char[] lineBuffer; + /** + * See the general contract of the readLine + * method of DataInput. + *

+ * Bytes + * for this operation are read from the contained + * input stream. + * + * @return the next line of text from this input stream. + * @throws IOException if an I/O error occurs. + * @see java.io.BufferedReader#readLine() + * @see java.io.FilterInputStream##in + * @deprecated This method does not properly convert bytes to characters. + * As of JDK 1.1, the preferred way to read lines of text is via the + * BufferedReader.readLine() method. Programs that use the + * DataInputStream class to read lines can be converted to use + * the BufferedReader class by replacing code of the form: + *

+     *     DataInputStream d = new DataInputStream(in);
+     * 
+ * with: + *
+     *     BufferedReader d
+     *          = new BufferedReader(new InputStreamReader(in));
+     * 
+ */ @Deprecated @Override public String readLine() throws IOException { @@ -260,109 +612,67 @@ public class AndroidRandomReadableFile implements DataInput, Closeable { return String.copyValueOf(buf, 0, offset); } + /** + * 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(); + + if (Build.VERSION.SDK_INT >= 28) { + //Reference.reachabilityFence(this); + } 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(); + } + } }