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
|
|
@ -6,12 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class M4aNoDash extends Postprocessing {
|
||||
|
||||
M4aNoDash(DownloadMission mission) {
|
||||
super(mission, 0, true);
|
||||
M4aNoDash() {
|
||||
super(0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class Mp4FromDashMuxer extends Postprocessing {
|
||||
|
||||
Mp4FromDashMuxer(DownloadMission mission) {
|
||||
super(mission, 2 * 1024 * 1024/* 2 MiB */, true);
|
||||
Mp4FromDashMuxer() {
|
||||
super(2 * 1024 * 1024/* 2 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
|
@ -9,9 +10,9 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.postprocessing.io.CircularFileWriter;
|
||||
import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
|
|
@ -20,30 +21,41 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
|||
|
||||
public abstract class Postprocessing {
|
||||
|
||||
static final byte OK_RESULT = ERROR_NOTHING;
|
||||
static transient final byte OK_RESULT = ERROR_NOTHING;
|
||||
|
||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, String[] args) {
|
||||
Postprocessing instance;
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
||||
if (null == algorithmName) {
|
||||
throw new NullPointerException("algorithmName");
|
||||
} else switch (algorithmName) {
|
||||
case ALGORITHM_TTML_CONVERTER:
|
||||
return new TtmlConverter(mission);
|
||||
instance = new TtmlConverter();
|
||||
break;
|
||||
case ALGORITHM_WEBM_MUXER:
|
||||
return new WebMMuxer(mission);
|
||||
instance = new WebMMuxer();
|
||||
break;
|
||||
case ALGORITHM_MP4_FROM_DASH_MUXER:
|
||||
return new Mp4FromDashMuxer(mission);
|
||||
instance = new Mp4FromDashMuxer();
|
||||
break;
|
||||
case ALGORITHM_M4A_NO_DASH:
|
||||
return new M4aNoDash(mission);
|
||||
instance = new M4aNoDash();
|
||||
break;
|
||||
/*case "example-algorithm":
|
||||
return new ExampleAlgorithm(mission);*/
|
||||
instance = new ExampleAlgorithm(mission);*/
|
||||
default:
|
||||
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
|
||||
}
|
||||
|
||||
instance.args = args;
|
||||
instance.name = algorithmName;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -61,32 +73,38 @@ public abstract class Postprocessing {
|
|||
/**
|
||||
* the download to post-process
|
||||
*/
|
||||
protected DownloadMission mission;
|
||||
protected transient DownloadMission mission;
|
||||
|
||||
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
||||
this.mission = mission;
|
||||
public transient File cacheDir;
|
||||
|
||||
private String[] args;
|
||||
|
||||
private String name;
|
||||
|
||||
Postprocessing(int recommendedReserve, boolean worksOnSameFile) {
|
||||
this.recommendedReserve = recommendedReserve;
|
||||
this.worksOnSameFile = worksOnSameFile;
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
File file = mission.getDownloadedFile();
|
||||
public void run(DownloadMission target) throws IOException {
|
||||
this.mission = target;
|
||||
|
||||
File temp = null;
|
||||
CircularFileWriter out = null;
|
||||
int result;
|
||||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = file.length();
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
for (SharpStream source : sources) source.rewind();
|
||||
|
|
@ -97,7 +115,7 @@ public abstract class Postprocessing {
|
|||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||
* or the CircularFileWriter can lead to unexpected results
|
||||
*/
|
||||
if (source.isDisposed() || source.available() < 1) {
|
||||
if (source.isClosed() || source.available() < 1) {
|
||||
continue;// the selected source is not used anymore
|
||||
}
|
||||
|
||||
|
|
@ -107,18 +125,19 @@ public abstract class Postprocessing {
|
|||
return -1;
|
||||
};
|
||||
|
||||
temp = new File(mission.location, mission.name + ".tmp");
|
||||
// TODO: use Context.getCache() for this operation
|
||||
temp = new File(cacheDir, mission.storage.getName() + ".tmp");
|
||||
|
||||
out = new CircularFileWriter(file, temp, checker);
|
||||
out = new CircularFileWriter(mission.storage.getStream(), temp, checker);
|
||||
out.onProgress = this::progressReport;
|
||||
|
||||
out.onWriteError = (err) -> {
|
||||
mission.postprocessingState = 3;
|
||||
mission.psState = 3;
|
||||
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
|
||||
|
||||
try {
|
||||
synchronized (this) {
|
||||
while (mission.postprocessingState == 3)
|
||||
while (mission.psState == 3)
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
|
|
@ -138,12 +157,12 @@ public abstract class Postprocessing {
|
|||
}
|
||||
} finally {
|
||||
for (SharpStream source : sources) {
|
||||
if (source != null && !source.isDisposed()) {
|
||||
source.dispose();
|
||||
if (source != null && !source.isClosed()) {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
out.close();
|
||||
}
|
||||
if (temp != null) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
|
|
@ -164,10 +183,9 @@ public abstract class Postprocessing {
|
|||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
if (result != OK_RESULT && worksOnSameFile) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
|
||||
|
||||
this.mission = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -192,11 +210,11 @@ public abstract class Postprocessing {
|
|||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
|
||||
String getArgumentAt(int index, String defaultValue) {
|
||||
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
|
||||
if (args == null || index >= args.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return mission.postprocessingArgs[index];
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
|
|
@ -209,4 +227,22 @@ public abstract class Postprocessing {
|
|||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
str.append(", ");
|
||||
str.append(arg);
|
||||
}
|
||||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.schabi.newpipe.streams.SubtitleConverter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -12,18 +12,15 @@ import java.text.ParseException;
|
|||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.postprocessing.io.SharpInputStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class TtmlConverter extends Postprocessing {
|
||||
private static final String TAG = "TtmlConverter";
|
||||
|
||||
TtmlConverter(DownloadMission mission) {
|
||||
TtmlConverter() {
|
||||
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||
super(mission, 0, true);
|
||||
super(0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class WebMMuxer extends Postprocessing {
|
||||
|
||||
WebMMuxer(DownloadMission mission) {
|
||||
super(mission, 2048 * 1024/* 2 MiB */, true);
|
||||
WebMMuxer() {
|
||||
super(2048 * 1024/* 2 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,150 +0,0 @@
|
|||
package us.shandian.giga.postprocessing.io;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
|
||||
private RandomAccessFile source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
|
||||
source = new RandomAccessFile(file, mode);
|
||||
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 dispose() {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException err) {
|
||||
} finally {
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,459 +0,0 @@
|
|||
package us.shandian.giga.postprocessing.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;
|
||||
|
||||
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(File source, 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(source);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the file without flushing any buffer
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
out = null;
|
||||
}
|
||||
if (aux != null) {
|
||||
aux.dispose();
|
||||
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 isDisposed() {
|
||||
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 RandomAccessFile target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
|
||||
private byte[] queue;
|
||||
private int queueSize;
|
||||
|
||||
BufferedFile(File file) throws FileNotFoundException {
|
||||
queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
target = new RandomAccessFile(file, "rw");
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void dispose() {
|
||||
try {
|
||||
queue = null;
|
||||
target.close();
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
protected 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;
|
||||
}
|
||||
|
||||
protected void reset() throws IOException {
|
||||
offset = 0;
|
||||
length = 0;
|
||||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
offset = absoluteOffset;
|
||||
target.seek(absoluteOffset);
|
||||
}
|
||||
|
||||
protected 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 absOffset;
|
||||
String absLength;
|
||||
|
||||
try {
|
||||
absOffset = Long.toString(target.getFilePointer());
|
||||
} catch (IOException e) {
|
||||
absOffset = "[" + e.getLocalizedMessage() + "]";
|
||||
}
|
||||
try {
|
||||
absLength = Long.toString(target.length());
|
||||
} catch (IOException e) {
|
||||
absLength = "[" + e.getLocalizedMessage() + "]";
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"offset=%s length=%s queue=%s absOffset=%s absLength=%s",
|
||||
offset, length, queueSize, absOffset, absLength
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* 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.postprocessing.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.dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue