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:
parent
1684a2110c
commit
9e34fee58c
49 changed files with 2715 additions and 1936 deletions
|
|
@ -164,9 +164,6 @@ public class DownloadInitializer extends Thread {
|
|||
}
|
||||
}
|
||||
|
||||
// hide marquee in the progress bar
|
||||
mMission.done++;
|
||||
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue