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

@ -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;