Implement Storage Access Framework
* re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
parent
8d8059229f
commit
7ca7952790
62 changed files with 2439 additions and 1180 deletions
148
app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java
Normal file
148
app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
throw new IOException("The chunk is empty or invalid");
|
||||
}
|
||||
if (source.length() < end) {
|
||||
try {
|
||||
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
source.seek(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute position on file
|
||||
*
|
||||
* @return the position
|
||||
*/
|
||||
public long getFilePointer() {
|
||||
return offset + position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if ((position + 1) > length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int res = source.read();
|
||||
if (res >= 0) {
|
||||
position++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long pos) throws IOException {
|
||||
pos = Math.min(pos + position, length);
|
||||
|
||||
if (pos == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
source.seek(offset + pos);
|
||||
|
||||
long oldPos = position;
|
||||
position = pos;
|
||||
|
||||
return pos - oldPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long available() {
|
||||
return (int) (length - position);
|
||||
}
|
||||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public void close() {
|
||||
source.close();
|
||||
source = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return source == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
position = 0;
|
||||
source.seek(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int count) {
|
||||
}
|
||||
|
||||
}
|
||||
451
app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
Normal file
451
app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CircularFileWriter extends SharpStream {
|
||||
|
||||
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
|
||||
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
|
||||
private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB
|
||||
|
||||
private OffsetChecker callback;
|
||||
|
||||
public ProgressReport onProgress;
|
||||
public WriteErrorHandle onWriteError;
|
||||
|
||||
private long reportPosition;
|
||||
private long maxLengthKnown = -1;
|
||||
|
||||
private BufferedFile out;
|
||||
private BufferedFile aux;
|
||||
|
||||
public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException {
|
||||
if (checker == null) {
|
||||
throw new NullPointerException("checker is null");
|
||||
}
|
||||
|
||||
if (!temp.exists()) {
|
||||
if (!temp.createNewFile()) {
|
||||
throw new IOException("Cannot create a temporal file");
|
||||
}
|
||||
}
|
||||
|
||||
aux = new BufferedFile(temp);
|
||||
out = new BufferedFile(target);
|
||||
|
||||
callback = checker;
|
||||
|
||||
reportPosition = NOTIFY_BYTES_INTERVAL;
|
||||
}
|
||||
|
||||
private void flushAuxiliar() throws IOException {
|
||||
if (aux.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean underflow = out.getOffset() >= out.length;
|
||||
|
||||
out.flush();
|
||||
aux.flush();
|
||||
|
||||
aux.target.seek(0);
|
||||
out.target.seek(out.length);
|
||||
|
||||
long length = aux.length;
|
||||
out.length += aux.length;
|
||||
|
||||
while (length > 0) {
|
||||
int read = (int) Math.min(length, Integer.MAX_VALUE);
|
||||
read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length));
|
||||
|
||||
out.writeProof(aux.queue, read);
|
||||
length -= read;
|
||||
}
|
||||
|
||||
if (underflow) {
|
||||
out.offset += aux.offset;
|
||||
out.target.seek(out.offset);
|
||||
} else {
|
||||
out.offset = out.length;
|
||||
}
|
||||
|
||||
if (out.length > maxLengthKnown) {
|
||||
maxLengthKnown = out.length;
|
||||
}
|
||||
|
||||
if (aux.length > THRESHOLD_AUX_LENGTH) {
|
||||
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
|
||||
}
|
||||
|
||||
aux.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any buffer and close the output file. Use this method if the
|
||||
* operation is successful
|
||||
*
|
||||
* @return the final length of the file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public long finalizeFile() throws IOException {
|
||||
flushAuxiliar();
|
||||
|
||||
out.flush();
|
||||
|
||||
// change file length (if required)
|
||||
long length = Math.max(maxLengthKnown, out.length);
|
||||
if (length != out.target.length()) {
|
||||
out.target.setLength(length);
|
||||
}
|
||||
|
||||
close();
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the file without flushing any buffer
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
out = null;
|
||||
}
|
||||
if (aux != null) {
|
||||
aux.close();
|
||||
aux = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b) throws IOException {
|
||||
write(new byte[]{b}, 0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long available;
|
||||
long offsetOut = out.getOffset();
|
||||
long offsetAux = aux.getOffset();
|
||||
long end = callback.check();
|
||||
|
||||
if (end == -1) {
|
||||
available = Integer.MAX_VALUE;
|
||||
} else if (end < offsetOut) {
|
||||
throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut));
|
||||
} else {
|
||||
available = end - offsetOut;
|
||||
}
|
||||
|
||||
boolean usingAux = aux.length > 0 && offsetOut >= out.length;
|
||||
boolean underflow = offsetAux < aux.length || offsetOut < out.length;
|
||||
|
||||
if (usingAux) {
|
||||
// before continue calculate the final length of aux
|
||||
long length = offsetAux + len;
|
||||
if (underflow) {
|
||||
if (aux.length > length) {
|
||||
length = aux.length;// the length is not changed
|
||||
}
|
||||
} else {
|
||||
length = aux.length + len;
|
||||
}
|
||||
|
||||
if (length > available || length < THRESHOLD_AUX_LENGTH) {
|
||||
aux.write(b, off, len);
|
||||
} else {
|
||||
if (underflow) {
|
||||
aux.write(b, off, len);
|
||||
flushAuxiliar();
|
||||
} else {
|
||||
flushAuxiliar();
|
||||
out.write(b, off, len);// write directly on the output
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (underflow) {
|
||||
available = out.length - offsetOut;
|
||||
}
|
||||
|
||||
int length = Math.min(len, (int) available);
|
||||
out.write(b, off, length);
|
||||
|
||||
len -= length;
|
||||
off += length;
|
||||
|
||||
if (len > 0) {
|
||||
aux.write(b, off, len);
|
||||
}
|
||||
}
|
||||
|
||||
if (onProgress != null) {
|
||||
long absoluteOffset = out.getOffset() + aux.getOffset();
|
||||
if (absoluteOffset > reportPosition) {
|
||||
reportPosition = absoluteOffset + NOTIFY_BYTES_INTERVAL;
|
||||
onProgress.report(absoluteOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
aux.flush();
|
||||
out.flush();
|
||||
|
||||
long total = out.length + aux.length;
|
||||
if (total > maxLengthKnown) {
|
||||
maxLengthKnown = total;// save the current file length in case the method {@code rewind()} is called
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
seek(out.getOffset() + aux.getOffset() + amount);
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||
}
|
||||
|
||||
seek(0);
|
||||
|
||||
reportPosition = NOTIFY_BYTES_INTERVAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long offset) throws IOException {
|
||||
long total = out.length + aux.length;
|
||||
if (offset == total) {
|
||||
return;// nothing to do
|
||||
}
|
||||
|
||||
// flush everything, avoid any underflow
|
||||
flush();
|
||||
|
||||
if (offset < 0 || offset > total) {
|
||||
throw new IOException("desired offset is outside of range=0-" + total + " offset=" + offset);
|
||||
}
|
||||
|
||||
if (offset > out.length) {
|
||||
out.seek(out.length);
|
||||
aux.seek(offset - out.length);
|
||||
} else {
|
||||
out.seek(offset);
|
||||
aux.seek(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return out == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeek() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="stub read methods">
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer
|
||||
) {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count
|
||||
) {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long available() {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
public interface OffsetChecker {
|
||||
|
||||
/**
|
||||
* Checks the amount of available space ahead
|
||||
*
|
||||
* @return absolute offset in the file where no more data SHOULD NOT be
|
||||
* written. If the value is -1 the whole file will be used
|
||||
*/
|
||||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
public interface WriteErrorHandle {
|
||||
|
||||
/**
|
||||
* Attempts to handle a I/O exception
|
||||
*
|
||||
* @param err the cause
|
||||
* @return {@code true} to retry and continue, otherwise, {@code false}
|
||||
* and throw the exception
|
||||
*/
|
||||
boolean handle(Exception err);
|
||||
}
|
||||
|
||||
class BufferedFile {
|
||||
|
||||
protected final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
|
||||
BufferedFile(File file) throws FileNotFoundException {
|
||||
this.target = new FileStream(file);
|
||||
}
|
||||
|
||||
BufferedFile(SharpStream target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
}
|
||||
|
||||
protected void write(byte b[], int off, int len) throws IOException {
|
||||
while (len > 0) {
|
||||
// if the queue is full, the method available() will flush the queue
|
||||
int read = Math.min(available(), len);
|
||||
|
||||
// enqueue incoming buffer
|
||||
System.arraycopy(b, off, queue, queueSize, read);
|
||||
queueSize += read;
|
||||
|
||||
len -= read;
|
||||
off += read;
|
||||
}
|
||||
|
||||
long total = offset + queueSize;
|
||||
if (total > length) {
|
||||
length = total;// save length
|
||||
}
|
||||
}
|
||||
|
||||
void flush() throws IOException {
|
||||
writeProof(queue, queueSize);
|
||||
offset += queueSize;
|
||||
queueSize = 0;
|
||||
}
|
||||
|
||||
protected void rewind() throws IOException {
|
||||
offset = 0;
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected int available() throws IOException {
|
||||
if (queueSize >= queue.length) {
|
||||
flush();
|
||||
return queue.length;
|
||||
}
|
||||
|
||||
return queue.length - queueSize;
|
||||
}
|
||||
|
||||
void reset() throws IOException {
|
||||
offset = 0;
|
||||
length = 0;
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
offset = absoluteOffset;
|
||||
target.seek(absoluteOffset);
|
||||
}
|
||||
|
||||
void writeProof(byte[] buffer, int length) throws IOException {
|
||||
if (onWriteError == null) {
|
||||
target.write(buffer, 0, length);
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
target.write(buffer, 0, length);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!onWriteError.handle(e)) {
|
||||
throw e;// give up
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String absLength;
|
||||
|
||||
try {
|
||||
absLength = Long.toString(target.length());
|
||||
} catch (IOException e) {
|
||||
absLength = "[" + e.getLocalizedMessage() + "]";
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"offset=%s length=%s queue=%s absLength=%s",
|
||||
offset, length, queueSize, absLength
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/us/shandian/giga/io/FileStream.java
Normal file
131
app/src/main/java/us/shandian/giga/io/FileStream.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class FileStream extends SharpStream {
|
||||
|
||||
public RandomAccessFile source;
|
||||
|
||||
public FileStream(@NonNull File target) throws FileNotFoundException {
|
||||
this.source = new RandomAccessFile(target, "rw");
|
||||
}
|
||||
|
||||
public FileStream(@NonNull String path) throws FileNotFoundException {
|
||||
this.source = new RandomAccessFile(path, "rw");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return source.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
return source.read(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
return source.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long pos) throws IOException {
|
||||
return source.skipBytes((int) pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long available() {
|
||||
try {
|
||||
return source.length() - source.getFilePointer();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (source == null) return;
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException err) {
|
||||
// nothing to do
|
||||
}
|
||||
source = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return source == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
source.seek(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeek() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSetLength() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte value) throws IOException {
|
||||
source.write(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
source.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||
source.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLength(long length) throws IOException {
|
||||
source.setLength(length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long offset) throws IOException {
|
||||
source.seek(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() throws IOException {
|
||||
return source.length();
|
||||
}
|
||||
}
|
||||
140
app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
Normal file
140
app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class FileStreamSAF extends SharpStream {
|
||||
|
||||
private final FileInputStream in;
|
||||
private final FileOutputStream out;
|
||||
private final FileChannel channel;
|
||||
private final ParcelFileDescriptor file;
|
||||
|
||||
private boolean disposed;
|
||||
|
||||
public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException {
|
||||
// Notes:
|
||||
// the file must exists first
|
||||
// ¡read-write mode must allow seek!
|
||||
// It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices
|
||||
|
||||
file = contentResolver.openFileDescriptor(fileUri, "rw");
|
||||
|
||||
if (file == null) {
|
||||
throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString());
|
||||
}
|
||||
|
||||
in = new FileInputStream(file.getFileDescriptor());
|
||||
out = new FileOutputStream(file.getFileDescriptor());
|
||||
channel = out.getChannel();// or use in.getChannel()
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return in.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return in.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
return in.read(buffer, offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
return in.skip(amount);// ¿or use channel.position(channel.position() + amount)?
|
||||
}
|
||||
|
||||
@Override
|
||||
public long available() {
|
||||
try {
|
||||
return in.available();
|
||||
} catch (IOException e) {
|
||||
return 0;// ¡but not -1!
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
seek(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
disposed = true;
|
||||
|
||||
file.close();
|
||||
in.close();
|
||||
out.close();
|
||||
channel.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("FileStreamSAF", "close() error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return disposed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canSetLength() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canSeek() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte value) throws IOException {
|
||||
out.write(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||
out.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public void setLength(long length) throws IOException {
|
||||
channel.truncate(length);
|
||||
}
|
||||
|
||||
public void seek(long offset) throws IOException {
|
||||
channel.position(offset);
|
||||
}
|
||||
}
|
||||
61
app/src/main/java/us/shandian/giga/io/SharpInputStream.java
Normal file
61
app/src/main/java/us/shandian/giga/io/SharpInputStream.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package us.shandian.giga.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Wrapper for the classic {@link java.io.InputStream}
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SharpInputStream extends InputStream {
|
||||
|
||||
private final SharpStream base;
|
||||
|
||||
public SharpInputStream(SharpStream base) throws IOException {
|
||||
if (!base.canRead()) {
|
||||
throw new IOException("The provided stream is not readable");
|
||||
}
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return base.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] bytes) throws IOException {
|
||||
return base.read(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
||||
return base.read(bytes, i, i1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long l) throws IOException {
|
||||
return base.skip(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
long res = base.available();
|
||||
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
base.close();
|
||||
}
|
||||
}
|
||||
175
app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
Normal file
175
app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class StoredDirectoryHelper {
|
||||
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
private File ioTree;
|
||||
private DocumentFile docTree;
|
||||
|
||||
private ContentResolver contentResolver;
|
||||
|
||||
private String tag;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.tag = tag;
|
||||
this.docTree = DocumentFile.fromTreeUri(context, path);
|
||||
|
||||
if (this.docTree == null)
|
||||
throw new IOException("Failed to create the tree from Uri");
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public StoredDirectoryHelper(@NonNull String location, String tag) {
|
||||
ioTree = new File(location);
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredFileHelper createFile(String filename, String mime) {
|
||||
StoredFileHelper storage;
|
||||
|
||||
try {
|
||||
if (docTree == null) {
|
||||
storage = new StoredFileHelper(ioTree, filename, tag);
|
||||
storage.sourceTree = Uri.fromFile(ioTree).toString();
|
||||
} else {
|
||||
storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag);
|
||||
storage.sourceTree = docTree.getUri().toString();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
storage.tag = tag;
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
public StoredFileHelper createUniqueFile(String filename, String mime) {
|
||||
ArrayList<String> existingNames = new ArrayList<>(50);
|
||||
|
||||
String ext;
|
||||
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
|
||||
ext = "";
|
||||
} else {
|
||||
ext = filename.substring(dotIndex);
|
||||
filename = filename.substring(0, dotIndex - 1);
|
||||
}
|
||||
|
||||
String name;
|
||||
if (docTree == null) {
|
||||
for (File file : ioTree.listFiles()) {
|
||||
name = file.getName().toLowerCase();
|
||||
if (name.startsWith(filename)) existingNames.add(name);
|
||||
}
|
||||
} else {
|
||||
for (DocumentFile file : docTree.listFiles()) {
|
||||
name = file.getName();
|
||||
if (name == null) continue;
|
||||
name = name.toLowerCase();
|
||||
if (name.startsWith(filename)) existingNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
boolean free = true;
|
||||
String lwFilename = filename.toLowerCase();
|
||||
for (String testName : existingNames) {
|
||||
if (testName.equals(lwFilename)) {
|
||||
free = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (free) return createFile(filename, mime);
|
||||
|
||||
String[] sortedNames = existingNames.toArray(new String[0]);
|
||||
Arrays.sort(sortedNames);
|
||||
|
||||
String newName;
|
||||
int downloadIndex = 0;
|
||||
do {
|
||||
newName = filename + " (" + downloadIndex + ")" + ext;
|
||||
++downloadIndex;
|
||||
if (downloadIndex == 1000) { // Probably an error on our side
|
||||
newName = System.currentTimeMillis() + ext;
|
||||
break;
|
||||
}
|
||||
} while (Arrays.binarySearch(sortedNames, newName) >= 0);
|
||||
|
||||
|
||||
return createFile(newName, mime);
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return docTree == null;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return docTree == null ? ioTree.exists() : docTree.exists();
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void acquirePermissions() throws IOException {
|
||||
if (docTree == null) return;
|
||||
|
||||
try {
|
||||
contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void revokePermissions() throws IOException {
|
||||
if (docTree == null) return;
|
||||
|
||||
try {
|
||||
contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Uri findFile(String filename) {
|
||||
if (docTree == null)
|
||||
return Uri.fromFile(new File(ioTree, filename));
|
||||
|
||||
// findFile() method is very slow
|
||||
DocumentFile file = docTree.findFile(filename);
|
||||
|
||||
return file == null ? null : file.getUri();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
|
||||
}
|
||||
|
||||
}
|
||||
301
app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
Normal file
301
app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
|
||||
public class StoredFileHelper implements Serializable {
|
||||
private static final long serialVersionUID = 0L;
|
||||
public static final String DEFAULT_MIME = "application/octet-stream";
|
||||
|
||||
private transient DocumentFile docFile;
|
||||
private transient DocumentFile docTree;
|
||||
private transient File ioFile;
|
||||
private transient ContentResolver contentResolver;
|
||||
|
||||
protected String source;
|
||||
String sourceTree;
|
||||
|
||||
protected String tag;
|
||||
|
||||
private String srcName;
|
||||
private String srcType;
|
||||
|
||||
public StoredFileHelper(String filename, String mime, String tag) {
|
||||
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
|
||||
|
||||
this.srcName = filename;
|
||||
this.srcType = mime == null ? DEFAULT_MIME : mime;
|
||||
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException {
|
||||
this.docTree = tree;
|
||||
this.contentResolver = contentResolver;
|
||||
|
||||
// this is very slow, because SAF does not allow overwrite
|
||||
DocumentFile res = this.docTree.findFile(filename);
|
||||
|
||||
if (res != null && res.exists() && res.isDirectory()) {
|
||||
if (!res.delete())
|
||||
throw new IOException("Directory with the same name found but cannot delete");
|
||||
res = null;
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename);
|
||||
if (res == null) throw new IOException("Cannot create the file");
|
||||
}
|
||||
|
||||
this.docFile = res;
|
||||
this.source = res.getUri().toString();
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException {
|
||||
this.source = path.toString();
|
||||
this.tag = tag;
|
||||
|
||||
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) {
|
||||
this.ioFile = new File(URI.create(this.source));
|
||||
} else {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||
if (file == null)
|
||||
throw new UnsupportedOperationException("Cannot get the file via SAF");
|
||||
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.docFile = file;
|
||||
|
||||
try {
|
||||
this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
}
|
||||
|
||||
public StoredFileHelper(File location, String filename, String tag) throws IOException {
|
||||
this.ioFile = new File(location, filename);
|
||||
this.tag = tag;
|
||||
|
||||
if (this.ioFile.exists()) {
|
||||
if (!this.ioFile.isFile() && !this.ioFile.delete())
|
||||
throw new IOException("The filename is already in use by non-file entity and cannot overwrite it");
|
||||
} else {
|
||||
if (!this.ioFile.createNewFile())
|
||||
throw new IOException("Cannot create the file");
|
||||
}
|
||||
|
||||
this.source = Uri.fromFile(this.ioFile).toString();
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
}
|
||||
|
||||
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
|
||||
if (storage.isInvalid())
|
||||
return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag);
|
||||
|
||||
StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag);
|
||||
|
||||
if (storage.sourceTree != null) {
|
||||
instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree));
|
||||
|
||||
if (instance.docTree == null)
|
||||
throw new IOException("Cannot deserialize the tree, ¿revoked permissions?");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) {
|
||||
// SAF notes:
|
||||
// ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files
|
||||
// ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType(mime)
|
||||
.putExtra(Intent.EXTRA_TITLE, filename)
|
||||
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS)
|
||||
.putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks
|
||||
|
||||
who.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public SharpStream getStream() throws IOException {
|
||||
invalid();
|
||||
|
||||
if (docFile == null)
|
||||
return new FileStream(ioFile);
|
||||
else
|
||||
return new FileStreamSAF(contentResolver, docFile.getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whatever if is possible access using the {@code java.io} API
|
||||
*
|
||||
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
|
||||
*/
|
||||
public boolean isDirect() {
|
||||
invalid();
|
||||
|
||||
return docFile == null;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return source == null;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
invalid();
|
||||
|
||||
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
|
||||
}
|
||||
|
||||
public void truncate() throws IOException {
|
||||
invalid();
|
||||
|
||||
try (SharpStream fs = getStream()) {
|
||||
fs.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean delete() {
|
||||
invalid();
|
||||
|
||||
if (docFile == null) return ioFile.delete();
|
||||
|
||||
boolean res = docFile.delete();
|
||||
|
||||
try {
|
||||
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
contentResolver.releasePersistableUriPermission(docFile.getUri(), flags);
|
||||
} catch (Exception ex) {
|
||||
// ¿what happen?
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public long length() {
|
||||
invalid();
|
||||
|
||||
return docFile == null ? ioFile.length() : docFile.length();
|
||||
}
|
||||
|
||||
public boolean canWrite() {
|
||||
if (source == null) return false;
|
||||
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
|
||||
}
|
||||
|
||||
public File getIOFile() {
|
||||
return ioFile;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (source == null) return srcName;
|
||||
return docFile == null ? ioFile.getName() : docFile.getName();
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
if (source == null) return srcType;
|
||||
return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public boolean existsAsFile() {
|
||||
if (source == null) return false;
|
||||
|
||||
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
|
||||
boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
|
||||
|
||||
return exists && asFile;
|
||||
}
|
||||
|
||||
public boolean create() {
|
||||
invalid();
|
||||
|
||||
if (docFile == null) {
|
||||
try {
|
||||
return ioFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (docTree == null || docFile.getName() == null) return false;
|
||||
|
||||
DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType());
|
||||
if (res == null) return false;
|
||||
|
||||
docFile = res;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
if (source == null) return;
|
||||
|
||||
srcName = getName();
|
||||
srcType = getType();
|
||||
|
||||
source = null;
|
||||
|
||||
sourceTree = null;
|
||||
docTree = null;
|
||||
docFile = null;
|
||||
ioFile = null;
|
||||
contentResolver = null;
|
||||
}
|
||||
|
||||
private void invalid() {
|
||||
if (source == null)
|
||||
throw new IllegalStateException("In invalid state");
|
||||
}
|
||||
|
||||
public boolean equals(StoredFileHelper storage) {
|
||||
if (this.isInvalid() != storage.isInvalid()) return false;
|
||||
if (this.isDirect() != storage.isDirect()) return false;
|
||||
|
||||
if (this.isDirect())
|
||||
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
|
||||
|
||||
return DocumentsContract.getDocumentId(
|
||||
this.docFile.getUri()
|
||||
).equalsIgnoreCase(DocumentsContract.getDocumentId(
|
||||
storage.docFile.getUri()
|
||||
));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
if (source == null)
|
||||
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
|
||||
else
|
||||
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue