New MP4 muxer + Queue changes + Storage fixes

Main changes:
* correctly check the available space (CircularFile.java)
* misc cleanup (CircularFile.java)
* use the "Error Reporter" for non-http errors
* rewrite network state checking and add better support for API 21 (Lollipop) or higher
* implement "metered networks"
* add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered"
* add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G
* rewrite DataReader ¡Webm muxer is now 57% more faster!
* rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices
* fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers

Download queue:
* remember queue status, unless the user pause the download (un-queue)
* semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts
* allow enqueue failed downloads
* new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time

Miscellaneous:
* fix crash while selecting details/error menu (mistake on MissionFragment.java)
* misc serialize changes (DownloadMission.java)
* minor UI tweaks
* allow overwrite paused downloads
* fix wrong icons for grid/list button in downloads
* add share option
* implement #2006
* correct misspelled word in strings.xml (es) (cmn)
* fix MissionAdapter crash during device shutdown

New Mp4Muxer + required changes:
* new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!!
* re-work in SharpStream
* drop mp4 dash muxer
* misc changes: add warning in SecondaryStreamHelper.java,
* strip m4a DASH files to normal m4a format (youtube only)

Fix storage issues:
* warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :)
* "write proof" allow post-processing resuming only if the device ran out of space
* implement "insufficient storage" error for downloads
This commit is contained in:
kapodamy 2019-03-22 22:54:07 -03:00
parent 1684a2110c
commit 9e34fee58c
49 changed files with 2715 additions and 1936 deletions

View file

@ -164,9 +164,6 @@ public class DownloadInitializer extends Thread {
}
}
// hide marquee in the progress bar
mMission.done++;
mMission.start();
}

View file

@ -40,6 +40,9 @@ public class DownloadMission extends Mission {
public static final int ERROR_UNKNOWN_HOST = 1005;
public static final int ERROR_CONNECT_HOST = 1006;
public static final int ERROR_POSTPROCESSING = 1007;
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
public static final int ERROR_HTTP_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
@ -83,8 +86,9 @@ public class DownloadMission extends Mission {
* 0: ready
* 1: running
* 2: completed
* 3: hold
*/
public int postprocessingState;
public volatile int postprocessingState;
/**
* Indicate if the post-processing algorithm works on the same file
@ -92,19 +96,19 @@ public class DownloadMission extends Mission {
public boolean postprocessingThis;
/**
* The current resource to download {@code urls[current]}
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
*/
public int current;
/**
* Metadata where the mission state is saved
*/
public File metadata;
public transient File metadata;
/**
* maximum attempts
*/
public int maxRetry;
public transient int maxRetry;
/**
* Approximated final length, this represent the sum of all resources sizes
@ -115,11 +119,11 @@ public class DownloadMission extends Mission {
boolean fallback;
private int finishCount;
public transient boolean running;
public transient boolean enqueued = true;
public boolean enqueued;
public int errCode = ERROR_NOTHING;
public transient Exception errObject = null;
public Exception errObject = null;
public transient boolean recovered;
public transient Handler mHandler;
private transient boolean mWritingToFile;
@ -131,7 +135,7 @@ public class DownloadMission extends Mission {
private transient boolean deleted;
int currentThreadCount;
private transient Thread[] threads = new Thread[0];
public transient volatile Thread[] threads = new Thread[0];
private transient Thread init = null;
@ -155,6 +159,8 @@ public class DownloadMission extends Mission {
this.location = location;
this.kind = kind;
this.offsets = new long[urls.length];
this.enqueued = true;
this.maxRetry = 3;
if (postprocessingName != null) {
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
@ -183,6 +189,7 @@ public class DownloadMission extends Mission {
*/
boolean isBlockPreserved(long block) {
checkBlock(block);
//noinspection ConstantConditions
return blockState.containsKey(block) ? blockState.get(block) : false;
}
@ -247,6 +254,12 @@ public class DownloadMission extends Mission {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(true);
// BUG workaround: switching between networks can freeze the download forever
//conn.setRequestProperty("Connection", "close");
conn.setConnectTimeout(30000);
conn.setReadTimeout(10000);
if (rangeStart >= 0) {
String req = "bytes=" + rangeStart + "-";
if (rangeEnd > 0) req += rangeEnd;
@ -342,11 +355,32 @@ public class DownloadMission extends Mission {
}
}
synchronized void notifyError(int code, Exception err) {
public synchronized void notifyError(int code, Exception err) {
Log.e(TAG, "notifyError() code = " + code, err);
if (err instanceof IOException) {
if (err.getMessage().contains("Permission denied")) {
code = ERROR_PERMISSION_DENIED;
err = null;
} else if (err.getMessage().contains("write failed: ENOSPC")) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
} else {
try {
File storage = new File(location);
if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
}
} catch (SecurityException e) {
// is a permission error
}
}
}
errCode = code;
errObject = err;
enqueued = false;
pause();
@ -378,6 +412,7 @@ public class DownloadMission extends Mission {
if (!doPostprocessing()) return;
enqueued = false;
running = false;
deleteThisFromFile();
@ -386,22 +421,20 @@ public class DownloadMission extends Mission {
}
private void notifyPostProcessing(int state) {
if (DEBUG) {
String action;
switch (state) {
case 1:
action = "Running";
break;
case 2:
action = "Completed";
break;
default:
action = "Failed";
}
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
String action;
switch (state) {
case 1:
action = "Running";
break;
case 2:
action = "Completed";
break;
default:
action = "Failed";
}
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
synchronized (blockState) {
// don't return without fully write the current state
postprocessingState = state;
@ -420,7 +453,6 @@ public class DownloadMission extends Mission {
if (threads != null)
for (Thread thread : threads) joinForThread(thread);
enqueued = false;
running = true;
errCode = ERROR_NOTHING;
@ -463,7 +495,7 @@ public class DownloadMission extends Mission {
}
/**
* Pause the mission, does not affect the blocks that are being downloaded.
* Pause the mission
*/
public synchronized void pause() {
if (!running) return;
@ -477,7 +509,6 @@ public class DownloadMission extends Mission {
running = false;
recovered = true;
enqueued = false;
if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt();
@ -514,7 +545,7 @@ public class DownloadMission extends Mission {
}
/**
* Removes the file and the meta file
* Removes the downloaded file and the meta file
*/
@Override
public boolean delete() {
@ -580,12 +611,21 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false
*/
public boolean isPsRunning() {
return postprocessingName != null && postprocessingState == 1;
return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3);
}
/**
* Indicated if the mission is ready
*
* @return true, otherwise, false
*/
public boolean isInitialized() {
return blocks >= 0; // DownloadMissionInitializer was executed
}
public long getLength() {
long calculated;
if (postprocessingState == 1) {
if (postprocessingState == 1 || postprocessingState == 3) {
calculated = length;
} else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
@ -596,13 +636,37 @@ public class DownloadMission extends Mission {
return calculated > nearLength ? calculated : nearLength;
}
/**
* set this mission state on the queue
*
* @param queue true to add to the queue, otherwise, false
*/
public void setEnqueued(boolean queue) {
enqueued = queue;
runAsync(-2, this::writeThisToFile);
}
/**
* Attempts to continue a blocked post-processing
*
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
*/
public void psContinue(boolean recover) {
postprocessingState = 1;
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
threads[0].interrupt();
}
private boolean doPostprocessing() {
if (postprocessingName == null || postprocessingState == 2) return true;
notifyPostProcessing(1);
notifyProgress(0);
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
if (DEBUG)
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
threads = new Thread[]{Thread.currentThread()};
Exception exception = null;

View file

@ -47,6 +47,11 @@ public abstract class Mission implements Serializable {
return new File(location, name);
}
/**
* Delete the downloaded file
*
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
*/
public boolean delete() {
deleted = true;
return getDownloadedFile().delete();

View file

@ -0,0 +1,43 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4DashReader;
import org.schabi.newpipe.streams.Mp4FromDashWriter;
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);
}
@Override
boolean test(SharpStream... sources) throws IOException {
// check if the mp4 file is DASH (youtube)
Mp4DashReader reader = new Mp4DashReader(sources[0]);
reader.parse();
switch (reader.getBrands()[0]) {
case 0x64617368:// DASH
case 0x69736F35:// ISO5
return true;
default:
return false;
}
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources[0]);
muxer.setMainBrand(0x4D344120);// binary string "M4A "
muxer.parseSources();
muxer.selectTracks(0);
muxer.build(out);
return OK_RESULT;
}
}

View file

@ -1,29 +1,29 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4DashWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class Mp4DashMuxer extends Postprocessing {
Mp4DashMuxer(DownloadMission mission) {
super(mission, 15360 * 1024/* 15 MiB */, true);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4DashWriter muxer = new Mp4DashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);
return OK_RESULT;
}
}
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4FromDashWriter;
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);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);
return OK_RESULT;
}
}

View file

@ -1,136 +0,0 @@
package us.shandian.giga.postprocessing;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaMuxer;
import android.media.MediaMuxer.OutputFormat;
import android.util.Log;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import us.shandian.giga.get.DownloadMission;
class Mp4Muxer extends Postprocessing {
private static final String TAG = "Mp4Muxer";
private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB
Mp4Muxer(DownloadMission mission) {
super(mission, 0, false);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
File dlFile = mission.getDownloadedFile();
File tmpFile = new File(mission.location, mission.name.concat(".tmp"));
if (tmpFile.exists())
if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION;
FileInputStream source = null;
MediaMuxer muxer = null;
//noinspection TryFinallyCanBeTryWithResources
try {
source = new FileInputStream(dlFile);
MediaExtractor tracks[] = {
getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]),
getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1])
};
muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
int tracksIndex[] = {
muxer.addTrack(tracks[0].getTrackFormat(0)),
muxer.addTrack(tracks[1].getTrackFormat(0))
};
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB
BufferInfo info = new BufferInfo();
long written = 0;
long nextReport = NOTIFY_BYTES_INTERVAL;
muxer.start();
while (true) {
int done = 0;
for (int i = 0; i < tracks.length; i++) {
if (tracksIndex[i] < 0) continue;
info.set(0,
tracks[i].readSampleData(buffer, 0),
tracks[i].getSampleTime(),
tracks[i].getSampleFlags()
);
if (info.size >= 0) {
muxer.writeSampleData(tracksIndex[i], buffer, info);
written += info.size;
done++;
}
if (!tracks[i].advance()) {
// EOF reached
tracks[i].release();
tracksIndex[i] = -1;
}
if (written > nextReport) {
nextReport = written + NOTIFY_BYTES_INTERVAL;
super.progressReport(written);
}
}
if (done < 1) break;
}
// this part should not fail
if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION;
return OK_RESULT;
} finally {
try {
if (muxer != null) {
muxer.stop();
muxer.release();
}
} catch (Exception err) {
if (DEBUG)
Log.e(TAG, "muxer stop/release failed", err);
}
if (source != null) {
try {
source.close();
} catch (IOException e) {
// nothing to do
}
}
// if the operation fails, delete the temporal file
if (tmpFile.exists()) {
//noinspection ResultOfMethodCallIgnored
tmpFile.delete();
}
}
}
private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(source.getFD(), offset, length);
extractor.selectTrack(0);
return extractor;
}
}

View file

@ -1,6 +1,7 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
@ -9,17 +10,22 @@ import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
import us.shandian.giga.postprocessing.io.CircularFile;
import us.shandian.giga.postprocessing.io.CircularFileWriter;
import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker;
import us.shandian.giga.service.DownloadManagerService;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing {
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
static final byte OK_RESULT = ERROR_NOTHING;
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
public static final String ALGORITHM_MP4_MUXER = "mp4";
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 static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) {
@ -27,14 +33,14 @@ public abstract class Postprocessing {
} else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
return new TtmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission);
case ALGORITHM_MP4_MUXER:
return new Mp4Muxer(mission);
case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission);
case ALGORITHM_MP4_FROM_DASH_MUXER:
return new Mp4FromDashMuxer(mission);
case ALGORITHM_M4A_NO_DASH:
return new M4aNoDash(mission);
/*case "example-algorithm":
return new ExampleAlgorithm(mission);*/
return new ExampleAlgorithm(mission);*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
}
@ -65,7 +71,8 @@ public abstract class Postprocessing {
public void run() throws IOException {
File file = mission.getDownloadedFile();
CircularFile out = null;
File temp = null;
CircularFileWriter out = null;
int result;
long finalLength = -1;
@ -81,29 +88,54 @@ public abstract class Postprocessing {
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore
if (test(sources)) {
for (SharpStream source : sources) source.rewind();
OffsetChecker checker = () -> {
for (ChunkFileInputStream source : sources) {
/*
* 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) {
continue;// the selected source is not used anymore
}
return source.getFilePointer() - 1;
}
return sources[idx[0]].getFilePointer() - 1;
}
return -1;
};
return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
temp = new File(mission.location, mission.name + ".tmp");
result = process(out, sources);
out = new CircularFileWriter(file, temp, checker);
out.onProgress = this::progressReport;
if (result == OK_RESULT)
finalLength = out.finalizeFile();
out.onWriteError = (err) -> {
mission.postprocessingState = 3;
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
try {
synchronized (this) {
while (mission.postprocessingState == 3)
wait();
}
} catch (InterruptedException e) {
// nothing to do
Log.e(this.getClass().getSimpleName(), "got InterruptedException");
}
return mission.errCode == ERROR_NOTHING;
};
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} else {
result = OK_RESULT;
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
@ -113,17 +145,22 @@ public abstract class Postprocessing {
if (out != null) {
out.dispose();
}
if (temp != null) {
//noinspection ResultOfMethodCallIgnored
temp.delete();
}
}
} else {
result = process(null);
result = test() ? process(null) : OK_RESULT;
}
if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length();
mission.done = finalLength;
mission.length = finalLength;
if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength;
}
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
@ -134,7 +171,18 @@ public abstract class Postprocessing {
}
/**
* Abstract method to execute the pos-processing algorithm
* Test if the post-processing algorithm can be skipped
*
* @param sources files to be processed
* @return {@code true} if the post-processing is required, otherwise, {@code false}
* @throws IOException if an I/O error occurs.
*/
boolean test(SharpStream... sources) throws IOException {
return true;
}
/**
* Abstract method to execute the post-processing algorithm
*
* @param out output stream
* @param sources files to be processed
@ -151,7 +199,7 @@ public abstract class Postprocessing {
return mission.postprocessingArgs[index];
}
void progressReport(long done) {
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;

View file

@ -94,7 +94,7 @@ public class ChunkFileInputStream extends SharpStream {
}
@Override
public int available() {
public long available() {
return (int) (length - position);
}
@ -147,7 +147,4 @@ public class ChunkFileInputStream extends SharpStream {
public void write(byte[] buffer, int offset, int count) {
}
@Override
public void flush() {
}
}

View file

@ -1,375 +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;
import java.util.ArrayList;
public class CircularFile extends SharpStream {
private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB
private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false;
private RandomAccessFile out;
private long position;
private long maxLengthKnown = -1;
private ArrayList<ManagedBuffer> auxiliaryBuffers;
private OffsetChecker callback;
private ManagedBuffer queue;
private long startOffset;
private ProgressReport onProgress;
private long reportPosition;
public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException {
if (checker == null) {
throw new NullPointerException("checker is null");
}
try {
queue = new ManagedBuffer(QUEUE_BUFFER_SIZE);
out = new RandomAccessFile(file, "rw");
out.seek(offset);
position = offset;
} catch (IOException err) {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
// nothing to do
}
throw err;
}
auxiliaryBuffers = new ArrayList<>(15);
callback = checker;
startOffset = offset;
reportPosition = offset;
onProgress = progressReport;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void dispose() {
try {
auxiliaryBuffers = null;
if (out != null) {
out.close();
out = null;
}
} catch (IOException err) {
// nothing to do
}
}
/**
* 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 {
flushEverything();
if (maxLengthKnown > -1) {
position = maxLengthKnown;
}
if (position < out.length()) {
out.setLength(position);
}
dispose();
return position;
}
@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 end = callback.check();
long available;
if (end == -1) {
available = Long.MAX_VALUE;
} else {
if (end < startOffset) {
throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end));
}
available = end - position;
}
// Check if possible flush one or more auxiliary buffer
if (auxiliaryBuffers.size() > 0) {
ManagedBuffer aux = auxiliaryBuffers.get(0);
// check if there is enough space to flush it completely
while (available >= (aux.size + queue.size)) {
available -= aux.size;
writeQueue(aux.buffer, 0, aux.size);
aux.dereference();
auxiliaryBuffers.remove(0);
if (auxiliaryBuffers.size() < 1) {
aux = null;
break;
}
aux = auxiliaryBuffers.get(0);
}
if (IMMEDIATE_AUX_BUFFER_FLUSH) {
// try partial flush to avoid allocate another auxiliary buffer
if (aux != null && aux.available() < len && available > queue.size) {
int size = Math.min(aux.size, (int) available - queue.size);
writeQueue(aux.buffer, 0, size);
aux.dereference(size);
available -= size;
}
}
}
if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) {
writeQueue(b, off, len);
} else {
int i = auxiliaryBuffers.size() - 1;
while (len > 0) {
if (i < 0) {
// allocate a new auxiliary buffer
auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE));
i++;
}
ManagedBuffer aux = auxiliaryBuffers.get(i);
available = aux.available();
if (available < 1) {
// secondary auxiliary buffer
available = len;
aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2));
auxiliaryBuffers.add(aux);
i++;
} else {
available = Math.min(len, available);
}
aux.write(b, off, (int) available);
len -= available;
if (len > 0) off += available;
}
}
}
private void writeOutside(byte buffer[], int offset, int length) throws IOException {
out.write(buffer, offset, length);
position += length;
if (onProgress != null && position > reportPosition) {
reportPosition = position + NOTIFY_BYTES_INTERVAL;
onProgress.report(position);
}
}
private void writeQueue(byte[] buffer, int offset, int length) throws IOException {
while (length > 0) {
if (queue.available() < length) {
flushQueue();
if (length >= queue.buffer.length) {
writeOutside(buffer, offset, length);
return;
}
}
int size = Math.min(queue.available(), length);
queue.write(buffer, offset, size);
offset += size;
length -= size;
}
if (queue.size >= queue.buffer.length) {
flushQueue();
}
}
private void flushQueue() throws IOException {
writeOutside(queue.buffer, 0, queue.size);
queue.size = 0;
}
private void flushEverything() throws IOException {
flushQueue();
if (auxiliaryBuffers.size() > 0) {
for (ManagedBuffer aux : auxiliaryBuffers) {
writeOutside(aux.buffer, 0, aux.size);
aux.dereference();
}
auxiliaryBuffers.clear();
}
}
/**
* Flush any buffer directly to the file. Warning: use this method ONLY if
* all read dependencies are disposed
*
* @throws IOException if the dependencies are not disposed
*/
@Override
public void flush() throws IOException {
if (callback.check() != -1) {
throw new IOException("All read dependencies of this file must be disposed first");
}
flushEverything();
// Save the current file length in case the method {@code rewind()} is called
if (position > maxLengthKnown) {
maxLengthKnown = position;
}
}
@Override
public void rewind() throws IOException {
flush();
out.seek(startOffset);
if (onProgress != null) {
onProgress.report(-position);
}
position = startOffset;
reportPosition = startOffset;
}
@Override
public long skip(long amount) throws IOException {
flush();
position += amount;
out.seek(position);
return amount;
}
@Override
public boolean isDisposed() {
return out == null;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canWrite() {
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 int 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 {
void report(long progress);
}
class ManagedBuffer {
byte[] buffer;
int size;
ManagedBuffer(int length) {
buffer = new byte[length];
}
void dereference() {
buffer = null;
size = 0;
}
void dereference(int amount) {
if (amount > size) {
throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")");
}
size -= amount;
System.arraycopy(buffer, amount, buffer, 0, size);
}
protected int available() {
return buffer.length - size;
}
private void write(byte[] b, int off, int len) {
System.arraycopy(b, off, buffer, size, len);
size += len;
}
@Override
public String toString() {
return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available());
}
}
}

View file

@ -0,0 +1,459 @@
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
);
}
}
}

View file

@ -1,126 +0,0 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* @author kapodamy
*/
public class FileStream extends SharpStream {
public enum Mode {
Read,
ReadWrite
}
public RandomAccessFile source;
private final Mode mode;
public FileStream(String path, Mode mode) throws IOException {
String flags;
if (mode == Mode.Read) {
flags = "r";
} else {
flags = "rw";
}
this.mode = mode;
source = new RandomAccessFile(path, flags);
}
@Override
public int read() throws IOException {
return source.read();
}
@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 {
return source.read(b, off, len);
}
@Override
public long skip(long pos) throws IOException {
FileChannel fc = source.getChannel();
fc.position(fc.position() + pos);
return pos;
}
@Override
public int available() {
try {
return (int) (source.length() - source.getFilePointer());
} catch (IOException ex) {
return 0;
}
}
@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 {
source.getChannel().position(0);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return mode == Mode.Read || mode == Mode.ReadWrite;
}
@Override
public boolean canWrite() {
return mode == Mode.ReadWrite;
}
@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 flush() {
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
}

View file

@ -14,6 +14,7 @@ import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
*
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
@ -49,7 +50,8 @@ public class SharpInputStream extends InputStream {
@Override
public int available() {
return base.available();
long res = base.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override

View file

@ -21,6 +21,8 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.get.sqlite.DownloadDataSource;
import us.shandian.giga.service.DownloadManagerService.DMChecker;
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -28,7 +30,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManager {
private static final String TAG = DownloadManager.class.getSimpleName();
enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating}
enum NetworkState {Unavailable, Operating, MeteredOperating}
public final static int SPECIAL_NOTHING = 0;
public final static int SPECIAL_PENDING = 1;
@ -45,7 +47,9 @@ public class DownloadManager {
private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
int mPrefMaxRetry;
boolean mPrefCrossNetwork;
boolean mPrefMeteredDownloads;
boolean mPrefQueueLimit;
private boolean mSelfMissionsControl;
/**
* Create a new instance
@ -152,8 +156,8 @@ public class DownloadManager {
}
mis.postprocessingState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
mis.errObject = new RuntimeException("stopped unexpectedly");
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
mis.errObject = null;
} else if (exists && !dl.isFile()) {
// probably a folder, this should never happens
if (!sub.delete()) {
@ -162,20 +166,21 @@ public class DownloadManager {
continue;
}
if (!exists) {
if (!exists && mis.isInitialized()) {
// downloaded file deleted, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount;
m.source = mis.source;
m.maxRetry = mis.maxRetry;
m.nearLength = mis.nearLength;
m.setEnqueued(mis.enqueued);
mis = m;
}
mis.running = false;
mis.recovered = exists;
mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler;
mMissionsPending.add(mis);
@ -205,17 +210,25 @@ public class DownloadManager {
synchronized (this) {
// check for existing pending download
DownloadMission pendingMission = getPendingMission(location, name);
if (pendingMission != null) {
// generate unique filename (?)
try {
name = generateUniqueName(location, name);
} catch (Exception e) {
Log.e(TAG, "Unable to generate unique name", e);
name = System.currentTimeMillis() + name;
Log.i(TAG, "Using " + name);
if (pendingMission.running) {
// generate unique filename (?)
try {
name = generateUniqueName(location, name);
} catch (Exception e) {
Log.e(TAG, "Unable to generate unique name", e);
name = System.currentTimeMillis() + name;
Log.i(TAG, "Using " + name);
}
} else {
// dispose the mission
mMissionsPending.remove(pendingMission);
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
pendingMission.delete();
}
} else {
// check for existing finished download
// check for existing finished download and dispose (if exists)
int index = getFinishedMissionIndex(location, name);
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
}
@ -242,14 +255,17 @@ public class DownloadManager {
mission.timestamp = System.currentTimeMillis();
}
mSelfMissionsControl = true;
mMissionsPending.add(mission);
// Before starting, save the state in case the internet connection is not available
// Before continue, save the metadata in case the internet connection is not available
Utility.writeToFile(mission.metadata, mission);
if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) {
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
if (canDownloadInCurrentNetwork() && start) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
}
}
}
@ -257,13 +273,14 @@ public class DownloadManager {
public void resumeMission(DownloadMission mission) {
if (!mission.running) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
}
}
public void pauseMission(DownloadMission mission) {
if (mission.running) {
mission.setEnqueued(false);
mission.pause();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
@ -335,7 +352,7 @@ public class DownloadManager {
int count = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running && !mission.isFinished() && !mission.isPsFailed())
if (mission.running && !mission.isPsFailed() && !mission.isFinished())
count++;
}
}
@ -343,10 +360,36 @@ public class DownloadManager {
return count;
}
void pauseAllMissions() {
public void pauseAllMissions(boolean force) {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) mission.pause();
for (DownloadMission mission : mMissionsPending) {
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
if (force) mission.threads = null;// avoid waiting for threads
mission.pause();
flag = true;
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
public void startAllMissions() {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running || mission.isPsFailed() || mission.isFinished()) continue;
flag = true;
mission.start();
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
}
@ -415,31 +458,35 @@ public class DownloadManager {
}
/**
* runs another mission in queue if possible
* runs one or multiple missions in from queue if possible
*
* @return true if exits pending missions running or a mission was started, otherwise, false
* @return true if one or multiple missions are running, otherwise, false
*/
boolean runAnotherMission() {
boolean runMissions() {
synchronized (this) {
if (mMissionsPending.size() < 1) return false;
int i = getRunningMissionsCount();
if (i > 0) return true;
if (!canDownloadInCurrentNetwork()) return false;
for (DownloadMission mission : mMissionsPending) {
if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) {
resumeMission(mission);
return true;
}
if (mPrefQueueLimit) {
for (DownloadMission mission : mMissionsPending)
if (!mission.isFinished() && mission.running) return true;
}
return false;
boolean flag = false;
for (DownloadMission mission : mMissionsPending) {
if (mission.running || !mission.enqueued || mission.isFinished()) continue;
resumeMission(mission);
if (mPrefQueueLimit) return true;
flag = true;
}
return flag;
}
}
public MissionIterator getIterator() {
mSelfMissionsControl = true;
return new MissionIterator();
}
@ -457,31 +504,43 @@ public class DownloadManager {
private boolean canDownloadInCurrentNetwork() {
if (mLastNetworkStatus == NetworkState.Unavailable) return false;
return !(mPrefCrossNetwork && mLastNetworkStatus == NetworkState.MobileOperating);
return !(mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating);
}
void handleConnectivityChange(NetworkState currentStatus) {
void handleConnectivityState(NetworkState currentStatus, boolean updateOnly) {
if (currentStatus == mLastNetworkStatus) return;
mLastNetworkStatus = currentStatus;
if (currentStatus == NetworkState.Unavailable) return;
if (currentStatus == NetworkState.Unavailable) {
return;
} else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) {
return;
if (!mSelfMissionsControl || updateOnly) {
return;// don't touch anything without the user interaction
}
boolean flag = false;
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
int running = 0;
int paused = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
flag = true;
if (mission.isFinished() || mission.isPsRunning()) continue;
if (mission.running && isMetered) {
paused++;
mission.pause();
} else if (!mission.running && !isMetered && mission.enqueued) {
running++;
mission.start();
if (mPrefQueueLimit) break;
}
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
if (running > 0) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
return;
}
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
void updateMaximumAttempts() {
@ -506,21 +565,24 @@ public class DownloadManager {
), Toast.LENGTH_LONG).show();
}
void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) {
boolean listed;
boolean finished = false;
void checkForRunningMission(String location, String name, DMChecker check) {
MissionCheck result = MissionCheck.None;
synchronized (this) {
DownloadMission mission = getPendingMission(location, name);
if (mission != null) {
listed = true;
DownloadMission pending = getPendingMission(location, name);
if (pending == null) {
if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished;
} else {
listed = getFinishedMissionIndex(location, name) >= 0;
finished = listed;
if (pending.isFinished()) {
result = MissionCheck.Finished;// this never should happen (race-condition)
} else {
result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending;
}
}
}
check.callback(listed, finished);
check.callback(result);
}
public class MissionIterator extends DiffUtil.Callback {
@ -592,39 +654,6 @@ public class DownloadManager {
return SPECIAL_NOTHING;
}
public MissionItem getItemUnsafe(int position) {
synchronized (DownloadManager.this) {
int count = mMissionsPending.size();
int count2 = mMissionsFinished.size();
if (count > 0) {
position--;
if (position == -1)
return new MissionItem(SPECIAL_PENDING);
else if (position < count)
return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position));
else if (position == count && count2 > 0)
return new MissionItem(SPECIAL_FINISHED);
else
position -= count;
} else {
if (count2 > 0 && position == 0) {
return new MissionItem(SPECIAL_FINISHED);
}
}
position--;
if (count2 < 1) {
throw new RuntimeException(
String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position)
);
}
return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position));
}
}
public void start() {
current = getSpecialItems();
@ -647,6 +676,32 @@ public class DownloadManager {
return hasFinished;
}
/**
* Check if exists missions running and paused. Corrupted and hidden missions are not counted
*
* @return two-dimensional array contains the current missions state.
* 1° entry: true if has at least one mission running
* 2° entry: true if has at least one mission paused
*/
public boolean[] hasValidPendingMissions() {
boolean running = false;
boolean paused = false;
synchronized (DownloadManager.this) {
for (DownloadMission mission : mMissionsPending) {
if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished())
continue;
if (mission.running)
paused = true;
else
running = true;
}
}
return new boolean[]{running, paused};
}
@Override
public int getOldListSize() {

View file

@ -15,7 +15,9 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@ -24,6 +26,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.PermissionChecker;
@ -48,7 +51,6 @@ public class DownloadManagerService extends Service {
private static final String TAG = "DownloadManagerService";
public static final int MESSAGE_RUNNING = 0;
public static final int MESSAGE_PAUSED = 1;
public static final int MESSAGE_FINISHED = 2;
public static final int MESSAGE_PROGRESS = 3;
@ -76,7 +78,7 @@ public class DownloadManagerService extends Service {
private Notification mNotification;
private Handler mHandler;
private boolean mForeground = false;
private NotificationManager notificationManager = null;
private NotificationManager mNotificationManager = null;
private boolean mDownloadNotificationEnable = true;
private int downloadDoneCount = 0;
@ -85,7 +87,9 @@ public class DownloadManagerService extends Service {
private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1);
private BroadcastReceiver mNetworkStateListener;
private ConnectivityManager mConnectivityManager;
private BroadcastReceiver mNetworkStateListener = null;
private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null;
private SharedPreferences mPrefs = null;
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange;
@ -147,25 +151,39 @@ public class DownloadManagerService extends Service {
.setContentText(getString(R.string.msg_running_detail));
mNotification = builder.build();
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNetworkStateListener = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
handleConnectivityChange(null);
return;
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
handleConnectivityState(false);
}
handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO));
}
};
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
@Override
public void onLost(Network network) {
handleConnectivityState(false);
}
};
mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL);
} else {
mNetworkStateListener = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleConnectivityState(false);
}
};
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network));
handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry));
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
mLock = new LockManager(this);
}
@ -173,12 +191,11 @@ public class DownloadManagerService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) {
if (intent == null) {
Log.d(TAG, "Restarting");
return START_NOT_STICKY;
}
Log.d(TAG, "Starting");
Log.d(TAG, intent == null ? "Restarting" : "Starting");
}
if (intent == null) return START_NOT_STICKY;
Log.i(TAG, "Got intent: " + intent);
String action = intent.getAction();
if (action != null) {
@ -193,6 +210,8 @@ public class DownloadManagerService extends Service {
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
handleConnectivityState(true);// first check the actual network status
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength));
} else if (downloadDoneNotification != null) {
@ -221,21 +240,25 @@ public class DownloadManagerService extends Service {
stopForeground(true);
if (notificationManager != null && downloadDoneNotification != null) {
if (mNotificationManager != null && downloadDoneNotification != null) {
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
}
mManager.pauseAllMissions();
manageLock(false);
unregisterReceiver(mNetworkStateListener);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL);
else
unregisterReceiver(mNetworkStateListener);
mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener);
if (icDownloadDone != null) icDownloadDone.recycle();
if (icDownloadFailed != null) icDownloadFailed.recycle();
if (icLauncher != null) icLauncher.recycle();
mManager.pauseAllMissions(true);
}
@Override
@ -264,15 +287,16 @@ public class DownloadManagerService extends Service {
notifyMediaScanner(mission.getDownloadedFile());
notifyFinishedDownload(mission.name);
mManager.setFinished(mission);
updateForegroundState(mManager.runAnotherMission());
handleConnectivityState(false);
updateForegroundState(mManager.runMissions());
break;
case MESSAGE_RUNNING:
case MESSAGE_PROGRESS:
updateForegroundState(true);
break;
case MESSAGE_ERROR:
notifyFailedDownload(mission);
updateForegroundState(mManager.runAnotherMission());
handleConnectivityState(false);
updateForegroundState(mManager.runMissions());
break;
case MESSAGE_PAUSED:
updateForegroundState(mManager.getRunningMissionsCount() > 0);
@ -293,36 +317,30 @@ public class DownloadManagerService extends Service {
}
}
private void handleConnectivityChange(NetworkInfo info) {
private void handleConnectivityState(boolean updateOnly) {
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
NetworkState status;
if (info == null) {
status = NetworkState.Unavailable;
Log.i(TAG, "actual connectivity status is unavailable");
} else if (!info.isAvailable() || !info.isConnected()) {
status = NetworkState.Unavailable;
Log.i(TAG, "actual connectivity status is not available and not connected");
Log.i(TAG, "Active network [connectivity is unavailable]");
} else {
int type = info.getType();
if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) {
status = NetworkState.MobileOperating;
} else if (type == ConnectivityManager.TYPE_WIFI) {
status = NetworkState.WifiOperating;
} else if (type == ConnectivityManager.TYPE_WIMAX ||
type == ConnectivityManager.TYPE_ETHERNET ||
type == ConnectivityManager.TYPE_BLUETOOTH) {
status = NetworkState.OtherOperating;
} else {
boolean connected = info.isConnected();
boolean metered = mConnectivityManager.isActiveNetworkMetered();
if (connected)
status = metered ? NetworkState.MeteredOperating : NetworkState.Operating;
else
status = NetworkState.Unavailable;
}
Log.i(TAG, "actual connectivity status is " + status.name());
Log.i(TAG, "Active network [connected=" + connected + " metered=" + metered + "] " + info.toString());
}
if (mManager == null) return;// avoid race-conditions while the service is starting
mManager.handleConnectivityChange(status);
mManager.handleConnectivityState(status, updateOnly);
}
private void handlePreferenceChange(SharedPreferences prefs, String key) {
private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) {
if (key.equals(getString(R.string.downloads_maximum_retry))) {
try {
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
@ -332,7 +350,9 @@ public class DownloadManagerService extends Service {
}
mManager.updateMaximumAttempts();
} else if (key.equals(getString(R.string.downloads_cross_network))) {
mManager.mPrefCrossNetwork = prefs.getBoolean(key, false);
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
}
}
@ -366,19 +386,20 @@ public class DownloadManagerService extends Service {
context.startService(intent);
}
public static void checkForRunningMission(Context context, String location, String name, DMChecker check) {
public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) {
Intent intent = new Intent();
intent.setClass(context, DownloadManagerService.class);
context.startService(intent);
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName cname, IBinder service) {
try {
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check);
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker);
} catch (Exception err) {
Log.w(TAG, "checkForRunningMission() callback is defective", err);
}
// TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download.
context.unbindService(this);
}
@ -389,7 +410,7 @@ public class DownloadManagerService extends Service {
}
public void notifyFinishedDownload(String name) {
if (!mDownloadNotificationEnable || notificationManager == null) {
if (!mDownloadNotificationEnable || mNotificationManager == null) {
return;
}
@ -428,7 +449,7 @@ public class DownloadManagerService extends Service {
downloadDoneNotification.setContentText(downloadDoneList);
}
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
downloadDoneCount++;
}
@ -458,7 +479,7 @@ public class DownloadManagerService extends Service {
.bigText(mission.name));
}
notificationManager.notify(id, downloadFailedNotification.build());
mNotificationManager.notify(id, downloadFailedNotification.build());
}
private PendingIntent makePendingIntent(String action) {
@ -487,7 +508,11 @@ public class DownloadManagerService extends Service {
mLockAcquired = acquire;
}
// Wrapper of DownloadManager
////////////////////////////////////////////////////////////////////////////////////////////////
// Wrappers for DownloadManager
////////////////////////////////////////////////////////////////////////////////////////////////
public class DMBinder extends Binder {
public DownloadManager getDownloadManager() {
return mManager;
@ -502,15 +527,15 @@ public class DownloadManagerService extends Service {
}
public void clearDownloadNotifications() {
if (notificationManager == null) return;
if (mNotificationManager == null) return;
if (downloadDoneNotification != null) {
notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
downloadDoneList.setLength(0);
downloadDoneCount = 0;
}
if (downloadFailedNotification != null) {
for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) {
notificationManager.cancel(downloadFailedNotificationID);
mNotificationManager.cancel(downloadFailedNotificationID);
}
mFailedDownloads.clear();
downloadFailedNotificationID++;
@ -524,7 +549,9 @@ public class DownloadManagerService extends Service {
}
public interface DMChecker {
void callback(boolean listed, boolean finished);
void callback(MissionCheck result);
}
public enum MissionCheck {None, Pending, PendingRunning, Finished}
}

View file

@ -14,15 +14,17 @@ import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.content.FileProvider;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -36,14 +38,17 @@ import android.widget.Toast;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
@ -57,10 +62,13 @@ import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
@ -69,6 +77,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
private static final String TAG = "MissionAdapter";
private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*";
static {
@ -85,9 +94,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
private Handler mHandler;
private MenuItem mClear;
private MenuItem mStartButton;
private MenuItem mPauseButton;
private View mEmptyMessage;
public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) {
public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) {
mContext = context;
mDownloadManager = downloadManager;
mDeleter = null;
@ -105,10 +116,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
onServiceMessage(msg);
break;
}
if (mStartButton != null && mPauseButton != null) switch (msg.what) {
case DownloadManagerService.MESSAGE_DELETED:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_PAUSED:
checkMasterButtonsVisibility();
break;
}
}
};
mClear = clearButton;
mEmptyMessage = emptyMessage;
mIterator = downloadManager.getIterator();
@ -225,8 +244,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long deltaDone = mission.done - h.lastDone;
boolean hasError = mission.errCode != ERROR_NOTHING;
// on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true
h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength));
// hide on error
// show if current resource length is not fetched
// show if length is unknown
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
float progress;
if (mission.unknownLength) {
@ -305,36 +326,64 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
}
private boolean viewWithFileProvider(@NonNull File file) {
if (!file.exists()) return true;
private void viewWithFileProvider(Mission mission) {
if (checkInvalidFile(mission)) return;
String ext = Utility.getFileExt(file.getName());
if (ext == null) return false;
String mimeType = resolveMimeType(mission);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file);
Uri uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.getDownloadedFile()
);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
}
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
}
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.v(TAG, "Starting intent: " + intent);
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
mContext.startActivity(intent);
} else {
Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG);
noPlayerToast.show();
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
}
}
private void shareFile(Mission mission) {
if (checkInvalidFile(mission)) return;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(resolveMimeType(mission));
intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI());
mContext.startActivity(Intent.createChooser(intent, null));
}
private static String resolveMimeType(@NonNull Mission mission) {
String ext = Utility.getFileExt(mission.getDownloadedFile().getName());
if (ext == null) return DEFAULT_MIME_TYPE;
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
}
private boolean checkInvalidFile(@NonNull Mission mission) {
if (mission.getDownloadedFile().exists()) return false;
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
return true;
}
@ -343,15 +392,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private void onServiceMessage(@NonNull Message msg) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
setAutoRefresh(true);
return;
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
break;
default:
return;
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
setAutoRefresh(true);
return;
}
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
@ -370,74 +413,98 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private void showError(@NonNull DownloadMission mission) {
StringBuilder str = new StringBuilder();
str.append(mContext.getString(R.string.label_code));
str.append(": ");
str.append(mission.errCode);
str.append('\n');
@StringRes int msg = R.string.general_error;
String msgEx = null;
switch (mission.errCode) {
case 416:
str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable));
msg = R.string.error_http_requested_range_not_satisfiable;
break;
case 404:
str.append(mContext.getString(R.string.error_http_not_found));
msg = R.string.error_http_not_found;
break;
case ERROR_NOTHING:
str.append("¿?");
break;
return;// this never should happen
case ERROR_FILE_CREATION:
str.append(mContext.getString(R.string.error_file_creation));
msg = R.string.error_file_creation;
break;
case ERROR_HTTP_NO_CONTENT:
str.append(mContext.getString(R.string.error_http_no_content));
msg = R.string.error_http_no_content;
break;
case ERROR_HTTP_UNSUPPORTED_RANGE:
str.append(mContext.getString(R.string.error_http_unsupported_range));
msg = R.string.error_http_unsupported_range;
break;
case ERROR_PATH_CREATION:
str.append(mContext.getString(R.string.error_path_creation));
msg = R.string.error_path_creation;
break;
case ERROR_PERMISSION_DENIED:
str.append(mContext.getString(R.string.permission_denied));
msg = R.string.permission_denied;
break;
case ERROR_SSL_EXCEPTION:
str.append(mContext.getString(R.string.error_ssl_exception));
msg = R.string.error_ssl_exception;
break;
case ERROR_UNKNOWN_HOST:
str.append(mContext.getString(R.string.error_unknown_host));
msg = R.string.error_unknown_host;
break;
case ERROR_CONNECT_HOST:
str.append(mContext.getString(R.string.error_connect_host));
msg = R.string.error_connect_host;
break;
case ERROR_POSTPROCESSING_STOPPED:
msg = R.string.error_postprocessing_stopped;
break;
case ERROR_POSTPROCESSING:
str.append(mContext.getString(R.string.error_postprocessing_failed));
case ERROR_UNKNOWN_EXCEPTION:
case ERROR_POSTPROCESSING_HOLD:
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
return;
case ERROR_INSUFFICIENT_STORAGE:
msg = R.string.error_insufficient_storage;
break;
case ERROR_UNKNOWN_EXCEPTION:
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
str = new StringBuilder(8);
str.append("HTTP ");
str.append(mission.errCode);
msgEx = "HTTP " + mission.errCode;
} else if (mission.errObject == null) {
str.append("(not_decelerated_error_code)");
msgEx = "(not_decelerated_error_code)";
} else {
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
return;
}
break;
}
if (mission.errObject != null) {
str.append("\n\n");
str.append(mission.errObject.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
if (msgEx != null)
builder.setMessage(msgEx);
else
builder.setMessage(msg);
// add report button for non-HTTP errors (range 100-599)
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
@StringRes final int mMsg = msg;
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
);
}
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(mission.name)
.setMessage(str)
.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
.setTitle(mission.name)
.create()
.show();
}
private void showError(Exception exception, UserAction action, @StringRes int reason) {
ErrorActivity.reportError(
mContext,
Collections.singletonList(exception),
null,
null,
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
);
}
public void clearFinishedDownloads() {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
@ -466,16 +533,24 @@ public class MissionAdapter extends Adapter<ViewHolder> {
showError(mission);
return true;
case R.id.queue:
h.queue.setChecked(!h.queue.isChecked());
mission.enqueued = h.queue.isChecked();
boolean flag = !h.queue.isChecked();
h.queue.setChecked(flag);
mission.setEnqueued(flag);
updateProgress(h);
return true;
case R.id.retry:
mission.psContinue(true);
return true;
case R.id.cancel:
mission.psContinue(false);
return false;
}
}
switch (id) {
case R.id.open:
return viewWithFileProvider(h.item.mission.getDownloadedFile());
case R.id.menu_item_share:
shareFile(h.item.mission);
return true;
case R.id.delete:
if (mDeleter == null) {
mDownloadManager.deleteMission(h.item.mission);
@ -529,15 +604,42 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
public void setClearButton(MenuItem clearButton) {
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
if (mClear == null)
clearButton.setVisible(mIterator.hasFinishedMissions());
mClear = clearButton;
}
public void setMasterButtons(MenuItem startButton, MenuItem pauseButton) {
boolean init = mStartButton == null || mPauseButton == null;
mStartButton = startButton;
mPauseButton = pauseButton;
if (init) checkMasterButtonsVisibility();
}
private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
}
private void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions();
mStartButton.setVisible(state[0]);
mPauseButton.setVisible(state[1]);
}
public void ensurePausedMissions() {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (((DownloadMission) h.item.mission).running) continue;
updateProgress(h);
h.lastTimeStamp = -1;
h.lastDone = -1;
}
}
public void deleterDispose(Bundle bundle) {
if (mDeleter != null) mDeleter.dispose(bundle);
@ -604,6 +706,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
ProgressDrawable progress;
PopupMenu popupMenu;
MenuItem retry;
MenuItem cancel;
MenuItem start;
MenuItem pause;
MenuItem open;
@ -636,22 +740,34 @@ public class MissionAdapter extends Adapter<ViewHolder> {
button.setOnClickListener(v -> showPopupMenu());
Menu menu = popupMenu.getMenu();
retry = menu.findItem(R.id.retry);
cancel = menu.findItem(R.id.cancel);
start = menu.findItem(R.id.start);
pause = menu.findItem(R.id.pause);
open = menu.findItem(R.id.open);
open = menu.findItem(R.id.menu_item_share);
queue = menu.findItem(R.id.queue);
showError = menu.findItem(R.id.error_message_view);
delete = menu.findItem(R.id.delete);
source = menu.findItem(R.id.source);
checksum = menu.findItem(R.id.checksum);
itemView.setOnClickListener((v) -> {
itemView.setHapticFeedbackEnabled(true);
itemView.setOnClickListener(v -> {
if (item.mission instanceof FinishedMission)
viewWithFileProvider(item.mission.getDownloadedFile());
viewWithFileProvider(item.mission);
});
itemView.setOnLongClickListener(v -> {
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
showPopupMenu();
return true;
});
}
private void showPopupMenu() {
retry.setVisible(false);
cancel.setVisible(false);
start.setVisible(false);
pause.setVisible(false);
open.setVisible(false);
@ -664,7 +780,16 @@ public class MissionAdapter extends Adapter<ViewHolder> {
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) {
if (!mission.isPsRunning()) {
if (mission.isPsRunning()) {
switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE:
case ERROR_POSTPROCESSING_HOLD:
retry.setVisible(true);
cancel.setVisible(true);
showError.setVisible(true);
break;
}
} else {
if (mission.running) {
pause.setVisible(true);
} else {

View file

@ -35,6 +35,8 @@ public class MissionsFragment extends Fragment {
private boolean mLinear;
private MenuItem mSwitch;
private MenuItem mClear = null;
private MenuItem mStart = null;
private MenuItem mPause = null;
private RecyclerView mList;
private View mEmpty;
@ -54,9 +56,11 @@ public class MissionsFragment extends Fragment {
mBinder = (DownloadManagerService.DMBinder) binder;
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty);
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(mBundle, getView());
setAdapterButtons();
mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger());
@ -132,7 +136,7 @@ public class MissionsFragment extends Fragment {
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity.getApplicationContext();
mContext = activity;
}
@ -154,7 +158,11 @@ public class MissionsFragment extends Fragment {
public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
if (mAdapter != null) mAdapter.setClearButton(mClear);
mStart = menu.findItem(R.id.start_downloads);
mPause = menu.findItem(R.id.pause_downloads);
if (mAdapter != null) setAdapterButtons();
super.onPrepareOptionsMenu(menu);
}
@ -168,6 +176,14 @@ public class MissionsFragment extends Fragment {
case R.id.clear_list:
mAdapter.clearFinishedDownloads();
return true;
case R.id.start_downloads:
item.setVisible(false);
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
item.setVisible(false);
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view
default:
return super.onOptionsItemSelected(item);
}
@ -193,9 +209,9 @@ public class MissionsFragment extends Fragment {
int icon;
if (mLinear)
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
else
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
else
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
mSwitch.setIcon(icon);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
@ -203,6 +219,13 @@ public class MissionsFragment extends Fragment {
}
}
private void setAdapterButtons() {
if (mClear == null || mStart == null || mPause == null) return;
mAdapter.setClearButton(mClear);
mAdapter.setMasterButtons(mStart, mPause);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);