MP4 muxer +misc modifications

* allow retry downloads with "post-processing failed" error in the new muxer
* MPEG-4 muxer  ¡¡ no DASH output!!
* keep the progress if download fails
* remove TODO in SecondaryStreamHelper.java
* misc clean-up
* delete TestAlgo.java
* delete ExtSDDownloadFailedActivity.java and remove it from AndroidManifest.xml
* use hardcored version for changing icon colors
This commit is contained in:
kapodamy 2019-01-21 01:30:03 -03:00
parent 684cb81974
commit f2285c0b19
15 changed files with 357 additions and 260 deletions

View File

@ -119,7 +119,6 @@
<activity <activity
android:name=".ReCaptchaActivity" android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity"/> android:label="@string/reCaptchaActivity"/>
<activity android:name=".download.ExtSDDownloadFailedActivity" />
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="android.support.v4.content.FileProvider"

View File

@ -457,7 +457,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
kind = 's'; kind = 's';
break; break;
default: default:
@ -477,7 +477,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
final String finalFileName = fileName; final String finalFileName = fileName;
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> { DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
// should be safe run the following code without "getActivity().runOnUiThread()"
if (listed) { if (listed) {
AlertDialog.Builder builder = new AlertDialog.Builder(context); AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title) builder.setTitle(R.string.download_dialog_title)
@ -511,11 +510,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (secondaryStream != null) { if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl(); secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null; psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections // set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize; nearLength = secondaryStream.getSizeInBytes() + videoSize;
} }

View File

@ -1,38 +0,0 @@
package org.schabi.newpipe.download;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
public class ExtSDDownloadFailedActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
}
@Override
protected void onStart() {
super.onStart();
new AlertDialog.Builder(this)
.setTitle(R.string.download_to_sdcard_error_title)
.setMessage(R.string.download_to_sdcard_error_message)
.setPositiveButton(R.string.yes, (DialogInterface dialogInterface, int i) -> {
NewPipeSettings.resetDownloadFolders(this);
finish();
})
.setNegativeButton(R.string.cancel, (DialogInterface dialogInterface, int i) -> {
dialogInterface.dismiss();
finish();
})
.create()
.show();
}
}

View File

@ -36,7 +36,6 @@ public class SecondaryStreamHelper<T extends Stream> {
* @return selected audio stream or null if a candidate was not found * @return selected audio stream or null if a candidate was not found
*/ */
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) { public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
// TODO: check if m4v and m4a selected streams are DASH compliant
switch (videoStream.getFormat()) { switch (videoStream.getFormat()) {
case WEBM: case WEBM:
case MPEG_4: case MPEG_4:

View File

@ -156,7 +156,6 @@ public class DownloadInitializer extends Thread {
if (retryCount++ > mMission.maxRetry) { if (retryCount++ > mMission.maxRetry) {
Log.e(TAG, "initializer failed", e); Log.e(TAG, "initializer failed", e);
mMission.running = false;
mMission.notifyError(e); mMission.notifyError(e);
return; return;
} }

View File

@ -39,7 +39,7 @@ public class DownloadMission extends Mission {
public static final int ERROR_SSL_EXCEPTION = 1004; public static final int ERROR_SSL_EXCEPTION = 1004;
public static final int ERROR_UNKNOWN_HOST = 1005; public static final int ERROR_UNKNOWN_HOST = 1005;
public static final int ERROR_CONNECT_HOST = 1006; public static final int ERROR_CONNECT_HOST = 1006;
public static final int ERROR_POSTPROCESSING_FAILED = 1007; public static final int ERROR_POSTPROCESSING = 1007;
public static final int ERROR_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
@ -79,9 +79,12 @@ public class DownloadMission extends Mission {
public String postprocessingName; public String postprocessingName;
/** /**
* Indicates if the post-processing algorithm is actually running, used to detect corrupt downloads * Indicates if the post-processing state:
* 0: ready
* 1: running
* 2: completed
*/ */
public boolean postprocessingRunning; public int postprocessingState;
/** /**
* Indicate if the post-processing algorithm works on the same file * Indicate if the post-processing algorithm works on the same file
@ -356,7 +359,7 @@ public class DownloadMission extends Mission {
finishCount++; finishCount++;
if (finishCount == currentThreadCount) { if (finishCount == currentThreadCount) {
if (errCode > ERROR_NOTHING) return; if (errCode != ERROR_NOTHING) return;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length); Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length);
@ -382,19 +385,26 @@ public class DownloadMission extends Mission {
} }
} }
private void notifyPostProcessing(boolean processing) { private void notifyPostProcessing(int state) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, (processing ? "enter" : "exit") + " 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) { synchronized (blockState) {
if (!processing) {
postprocessingName = null;
postprocessingArgs = null;
}
// don't return without fully write the current state // don't return without fully write the current state
postprocessingRunning = processing; postprocessingState = state;
Utility.writeToFile(metadata, DownloadMission.this); Utility.writeToFile(metadata, DownloadMission.this);
} }
} }
@ -403,7 +413,7 @@ public class DownloadMission extends Mission {
* Start downloading with multiple threads. * Start downloading with multiple threads.
*/ */
public void start() { public void start() {
if (running || current >= urls.length) return; if (running || isFinished()) return;
// ensure that the previous state is completely paused. // ensure that the previous state is completely paused.
joinForThread(init); joinForThread(init);
@ -414,6 +424,19 @@ public class DownloadMission extends Mission {
running = true; running = true;
errCode = ERROR_NOTHING; errCode = ERROR_NOTHING;
if (current >= urls.length && postprocessingName != null) {
runAsync(1, () -> {
if (doPostprocessing()) {
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
}
});
return;
}
if (blocks < 0) { if (blocks < 0) {
initializer(); initializer();
return; return;
@ -445,18 +468,18 @@ public class DownloadMission extends Mission {
public synchronized void pause() { public synchronized void pause() {
if (!running) return; if (!running) return;
running = false; if (isPsRunning()) {
recovered = true;
enqueued = false;
if (postprocessingRunning) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "pause during post-processing is not applicable."); Log.w(TAG, "pause during post-processing is not applicable.");
} }
return; return;
} }
if (init != null && init.isAlive()) { running = false;
recovered = true;
enqueued = false;
if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt(); init.interrupt();
synchronized (blockState) { synchronized (blockState) {
resetState(); resetState();
@ -533,13 +556,36 @@ public class DownloadMission extends Mission {
mWritingToFile = false; mWritingToFile = false;
} }
/**
* Indicates if the download if fully finished
*
* @return true, otherwise, false
*/
public boolean isFinished() { public boolean isFinished() {
return current >= urls.length && postprocessingName == null; return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
}
/**
* Indicates if the download file is corrupt due a failed post-processing
*
* @return {@code true} if this mission is unrecoverable
*/
public boolean isPsFailed() {
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
}
/**
* Indicates if a post-processing algorithm is running
*
* @return true, otherwise, false
*/
public boolean isPsRunning() {
return postprocessingName != null && postprocessingState == 1;
} }
public long getLength() { public long getLength() {
long calculated; long calculated;
if (postprocessingRunning) { if (postprocessingState == 1) {
calculated = length; calculated = length;
} else { } else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
@ -551,16 +597,19 @@ public class DownloadMission extends Mission {
} }
private boolean doPostprocessing() { private boolean doPostprocessing() {
if (postprocessingName == null) return true; if (postprocessingName == null || postprocessingState == 2) return true;
notifyPostProcessing(1);
notifyProgress(0);
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
Exception exception = null;
try { try {
notifyPostProcessing(true); Postprocessing
notifyProgress(0); .getAlgorithm(postprocessingName, this)
.run();
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this);
algorithm.run();
} catch (Exception err) { } catch (Exception err) {
StringBuilder args = new StringBuilder(" "); StringBuilder args = new StringBuilder(" ");
if (postprocessingArgs != null) { if (postprocessingArgs != null) {
@ -572,15 +621,21 @@ public class DownloadMission extends Mission {
} }
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err); Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
notifyError(ERROR_POSTPROCESSING_FAILED, err); if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
return false;
exception = err;
} finally { } finally {
notifyPostProcessing(false); notifyPostProcessing(errCode == ERROR_NOTHING ? 2 : 0);
} }
if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR); if (errCode != ERROR_NOTHING) {
if (exception == null) exception = errObject;
notifyError(ERROR_POSTPROCESSING, exception);
return errCode == ERROR_NOTHING; return false;
}
return true;
} }
private boolean deleteThisFromFile() { private boolean deleteThisFromFile() {

View File

@ -13,9 +13,7 @@ import us.shandian.giga.get.DownloadMission;
class Mp4DashMuxer extends Postprocessing { class Mp4DashMuxer extends Postprocessing {
Mp4DashMuxer(DownloadMission mission) { Mp4DashMuxer(DownloadMission mission) {
super(mission); super(mission, 15360 * 1024/* 15 MiB */, true);
recommendedReserve = 15360 * 1024;// 15 MiB
worksOnSameFile = true;
} }
@Override @Override

View File

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

View File

@ -18,21 +18,21 @@ public abstract class Postprocessing {
public static final String ALGORITHM_TTML_CONVERTER = "ttml"; public static final String ALGORITHM_TTML_CONVERTER = "ttml";
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D"; 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_WEBM_MUXER = "webm";
private static final String ALGORITHM_TEST_ALGO = "test";
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) { if (null == algorithmName) {
throw new NullPointerException("algorithmName"); throw new NullPointerException("algorithmName");
} else switch (algorithmName) { } else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER: case ALGORITHM_TTML_CONVERTER:
return new TttmlConverter(mission); return new TtmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER: case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission); return new Mp4DashMuxer(mission);
case ALGORITHM_MP4_MUXER:
return new Mp4Muxer(mission);
case ALGORITHM_WEBM_MUXER: case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission); return new WebMMuxer(mission);
case ALGORITHM_TEST_ALGO:
return new TestAlgo(mission);
/*case "example-algorithm": /*case "example-algorithm":
return new ExampleAlgorithm(mission);*/ return new ExampleAlgorithm(mission);*/
default: default:
@ -52,71 +52,84 @@ public abstract class Postprocessing {
*/ */
public int recommendedReserve; public int recommendedReserve;
/**
* the download to post-process
*/
protected DownloadMission mission; protected DownloadMission mission;
Postprocessing(DownloadMission mission) { Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
this.mission = mission; this.mission = mission;
this.recommendedReserve = recommendedReserve;
this.worksOnSameFile = worksOnSameFile;
} }
public void run() throws IOException { public void run() throws IOException {
File file = mission.getDownloadedFile(); File file = mission.getDownloadedFile();
CircularFile out = null; CircularFile out = null;
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; int result;
long finalLength = -1;
try { mission.done = 0;
int i = 0; mission.length = file.length();
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0}; if (worksOnSameFile) {
CircularFile.OffsetChecker checker = () -> { ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
while (idx[0] < sources.length) { try {
/* int i = 0;
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks) for (; i < sources.length - 1; i++) {
* or the CircularFile can lead to unexpected results sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
*/ }
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
idx[0]++;
continue;// the selected source is not used anymore 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
}
return sources[idx[0]].getFilePointer() - 1;
} }
return sources[idx[0]].getFilePointer() - 1; return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
}
} }
if (out != null) {
return -1; out.dispose();
};
out = new CircularFile(file, 0, this::progressReport, checker);
mission.done = 0;
mission.length = file.length();
int result = process(out, sources);
if (result == OK_RESULT) {
long finalLength = out.finalizeFile();
mission.done = finalLength;
mission.length = finalLength;
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) {
//noinspection ResultOfMethodCallIgnored
new File(mission.location, mission.name).delete();
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
} }
} }
if (out != null) { } else {
out.dispose(); result = process(null);
} }
if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length();
mission.done = finalLength;
mission.length = finalLength;
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) {
//noinspection ResultOfMethodCallIgnored
file.delete();
} }
} }
@ -138,7 +151,7 @@ public abstract class Postprocessing {
return mission.postprocessingArgs[index]; return mission.postprocessingArgs[index];
} }
private void progressReport(long done) { void progressReport(long done) {
mission.done = done; mission.done = done;
if (mission.length < mission.done) mission.length = mission.done; if (mission.length < mission.done) mission.length = mission.done;

View File

@ -1,54 +0,0 @@
package us.shandian.giga.postprocessing;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.util.Random;
import us.shandian.giga.get.DownloadMission;
/**
* Algorithm for testing proposes
*/
class TestAlgo extends Postprocessing {
public TestAlgo(DownloadMission mission) {
super(mission);
worksOnSameFile = true;
recommendedReserve = 4096 * 1024;// 4 KiB
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
int written = 0;
int size = 5 * 1024 * 1024;// 5 MiB
byte[] buffer = new byte[8 * 1024];//8 KiB
mission.length = size;
Random rnd = new Random();
// only write random data
sources[0].dispose();
while (written < size) {
rnd.nextBytes(buffer);
int read = Math.min(buffer.length, size - written);
out.write(buffer, 0, read);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
return -1;
}
written += read;
}
return Postprocessing.OK_RESULT;
}
}

View File

@ -18,13 +18,12 @@ import us.shandian.giga.postprocessing.io.SharpInputStream;
/** /**
* @author kapodamy * @author kapodamy
*/ */
class TttmlConverter extends Postprocessing { class TtmlConverter extends Postprocessing {
private static final String TAG = "TttmlConverter"; private static final String TAG = "TtmlConverter";
TttmlConverter(DownloadMission mission) { TtmlConverter(DownloadMission mission) {
super(mission); // due how XmlPullParser works, the xml is fully loaded on the ram
recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram super(mission, 0, true);
worksOnSameFile = true;
} }
@Override @Override
@ -41,7 +40,7 @@ class TttmlConverter extends Postprocessing {
out, out,
getArgumentAt(1, "true").equals("true"), getArgumentAt(1, "true").equals("true"),
getArgumentAt(2, "true").equals("true") getArgumentAt(2, "true").equals("true")
); );
} catch (Exception err) { } catch (Exception err) {
Log.e(TAG, "subtitle parse failed", err); Log.e(TAG, "subtitle parse failed", err);
@ -56,7 +55,7 @@ class TttmlConverter extends Postprocessing {
} else if (err instanceof XPathExpressionException) { } else if (err instanceof XPathExpressionException) {
return 7; return 7;
} }
return 8; return 8;
} }

View File

@ -15,9 +15,7 @@ import us.shandian.giga.get.DownloadMission;
class WebMMuxer extends Postprocessing { class WebMMuxer extends Postprocessing {
WebMMuxer(DownloadMission mission) { WebMMuxer(DownloadMission mission) {
super(mission); super(mission, 2048 * 1024/* 2 MiB */, true);
recommendedReserve = 2048 * 1024;// 2 MiB
worksOnSameFile = true;
} }
@Override @Override

View File

@ -141,15 +141,18 @@ public class DownloadManager {
File dl = mis.getDownloadedFile(); File dl = mis.getDownloadedFile();
boolean exists = dl.exists(); boolean exists = dl.exists();
if (mis.postprocessingRunning && mis.postprocessingThis) { if (mis.isPsRunning()) {
// Incomplete post-processing results in a corrupted download file if (mis.postprocessingThis) {
// because the selected algorithm works on the same file to save space. // Incomplete post-processing results in a corrupted download file
if (!dl.delete()) { // because the selected algorithm works on the same file to save space.
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); if (exists && dl.isFile() && !dl.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
} }
exists = true;
mis.postprocessingRunning = false; mis.postprocessingState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED; mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
mis.errObject = new RuntimeException("stopped unexpectedly"); mis.errObject = new RuntimeException("stopped unexpectedly");
} else if (exists && !dl.isFile()) { } else if (exists && !dl.isFile()) {
// probably a folder, this should never happens // probably a folder, this should never happens
@ -332,7 +335,7 @@ public class DownloadManager {
int count = 0; int count = 0;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished()) if (mission.running && !mission.isFinished() && !mission.isPsFailed())
count++; count++;
} }
} }
@ -471,7 +474,7 @@ public class DownloadManager {
boolean flag = false; boolean flag = false;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.running && mission.isFinished() && !mission.postprocessingRunning) { if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
flag = true; flag = true;
mission.pause(); mission.pause();
} }
@ -528,6 +531,8 @@ public class DownloadManager {
ArrayList<Object> current; ArrayList<Object> current;
ArrayList<Mission> hidden; ArrayList<Mission> hidden;
boolean hasFinished = false;
private MissionIterator() { private MissionIterator() {
hidden = new ArrayList<>(2); hidden = new ArrayList<>(2);
current = null; current = null;
@ -563,6 +568,7 @@ public class DownloadManager {
list.addAll(finished); list.addAll(finished);
} }
hasFinished = finished.size() > 0;
return list; return list;
} }
@ -637,6 +643,10 @@ public class DownloadManager {
hidden.remove(mission); hidden.remove(mission);
} }
public boolean hasFinishedMissions() {
return hasFinished;
}
@Override @Override
public int getOldListSize() { public int getOldListSize() {

View File

@ -59,7 +59,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; 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_PATH_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_FAILED; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; 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_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
@ -67,7 +67,8 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
public class MissionAdapter extends Adapter<ViewHolder> { public class MissionAdapter extends Adapter<ViewHolder> {
private static final SparseArray<String> ALGORITHMS = new SparseArray<>(); private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
private static final String TAG = "MissionAdapter"; private static final String TAG = "MissionAdapter";
private static final String UNDEFINED_SPEED = "--.-%"; private static final String UNDEFINED_PROGRESS = "--.-%";
static { static {
ALGORITHMS.put(R.id.md5, "MD5"); ALGORITHMS.put(R.id.md5, "MD5");
@ -178,7 +179,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (h.item.mission instanceof DownloadMission) { if (h.item.mission instanceof DownloadMission) {
DownloadMission mission = (DownloadMission) item.mission; DownloadMission mission = (DownloadMission) item.mission;
String length = Utility.formatBytes(mission.getLength()); String length = Utility.formatBytes(mission.getLength());
if (mission.running && !mission.postprocessingRunning) length += " --.- kB/s"; if (mission.running && !mission.isPsRunning()) length += " --.- kB/s";
h.size.setText(length); h.size.setText(length);
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
@ -238,11 +239,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
} }
if (hasError) { if (hasError) {
if (Float.isNaN(progress) || Float.isInfinite(progress)) h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
h.progress.setProgress(1f);
h.status.setText(R.string.msg_error); h.status.setText(R.string.msg_error);
} else if (Float.isNaN(progress) || Float.isInfinite(progress)) { } else if (isNotFinite(progress)) {
h.status.setText(UNDEFINED_SPEED); h.status.setText(UNDEFINED_PROGRESS);
} else { } else {
h.status.setText(String.format("%.2f%%", progress * 100)); h.status.setText(String.format("%.2f%%", progress * 100));
h.progress.setProgress(progress); h.progress.setProgress(progress);
@ -251,11 +251,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long length = mission.getLength(); long length = mission.getLength();
int state; int state;
if (mission.errCode == ERROR_POSTPROCESSING_FAILED) { if (mission.isPsFailed()) {
state = 0; state = 0;
} else if (!mission.running) { } else if (!mission.running) {
state = mission.enqueued ? 1 : 2; state = mission.enqueued ? 1 : 2;
} else if (mission.postprocessingRunning) { } else if (mission.isPsRunning()) {
state = 3; state = 3;
} else { } else {
state = 0; state = 0;
@ -406,7 +406,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
case ERROR_CONNECT_HOST: case ERROR_CONNECT_HOST:
str.append(mContext.getString(R.string.error_connect_host)); str.append(mContext.getString(R.string.error_connect_host));
break; break;
case ERROR_POSTPROCESSING_FAILED: case ERROR_POSTPROCESSING:
str.append(mContext.getString(R.string.error_postprocessing_failed)); str.append(mContext.getString(R.string.error_postprocessing_failed));
case ERROR_UNKNOWN_EXCEPTION: case ERROR_UNKNOWN_EXCEPTION:
break; break;
@ -447,7 +447,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (mission != null) { if (mission != null) {
switch (id) { switch (id) {
case R.id.start: case R.id.start:
h.status.setText(UNDEFINED_SPEED); h.status.setText(UNDEFINED_PROGRESS);
h.state = -1; h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength())); h.size.setText(Utility.formatBytes(mission.getLength()));
mDownloadManager.resumeMission(mission); mDownloadManager.resumeMission(mission);
@ -507,7 +507,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
mIterator.end(); mIterator.end();
checkEmptyMessageVisibility(); checkEmptyMessageVisibility();
checkClearButtonVisibility(mClear); mClear.setVisible(mIterator.hasFinishedMissions());
} }
public void forceUpdate() { public void forceUpdate() {
@ -526,20 +526,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
} }
public void setClearButton(MenuItem clearButton) { public void setClearButton(MenuItem clearButton) {
if (mClear == null) checkClearButtonVisibility(clearButton); if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
mClear = clearButton; mClear = clearButton;
} }
private void checkClearButtonVisibility(MenuItem clearButton) {
if (mIterator.getOldListSize() < 1) {
clearButton.setVisible(false);
return;
}
DownloadManager.MissionItem item = mIterator.getItem(mIterator.getOldListSize() - 1);
clearButton.setVisible(item.special == DownloadManager.SPECIAL_FINISHED || item.mission instanceof FinishedMission);
}
private void checkEmptyMessageVisibility() { private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
@ -596,6 +586,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
} }
} }
private boolean isNotFinite(Float value) {
return Float.isNaN(value) || Float.isInfinite(value);
}
class ViewHolderItem extends RecyclerView.ViewHolder { class ViewHolderItem extends RecyclerView.ViewHolder {
DownloadManager.MissionItem item; DownloadManager.MissionItem item;
@ -667,7 +661,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) { if (mission != null) {
if (!mission.postprocessingRunning) { if (!mission.isPsRunning()) {
if (mission.running) { if (mission.running) {
pause.setVisible(true); pause.setVisible(true);
} else { } else {
@ -678,8 +672,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
queue.setChecked(mission.enqueued); queue.setChecked(mission.enqueued);
delete.setVisible(true); delete.setVisible(true);
start.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED);
queue.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED); boolean flag = !mission.isPsFailed();
start.setVisible(flag);
queue.setVisible(flag);
} }
} }
} else { } else {

View File

@ -7,16 +7,12 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.AttrRes;
import android.support.annotation.DrawableRes;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -24,6 +20,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
@ -192,29 +189,20 @@ public class MissionsFragment extends Fragment {
mList.setAdapter(mAdapter); mList.setAdapter(mAdapter);
if (mSwitch != null) { if (mSwitch != null) {
mSwitch.setIcon(getDrawableFromAttribute(mLinear ? R.attr.ic_grid : R.attr.ic_list)); boolean isLight = ThemeHelper.isLightThemeSelected(mContext);
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;
mSwitch.setIcon(icon);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
mPrefs.edit().putBoolean("linear", mLinear).apply(); mPrefs.edit().putBoolean("linear", mLinear).apply();
} }
} }
@DrawableRes
private int getDrawableFromAttribute(@AttrRes int attr) {
TypedArray styledAttributes = mContext.getTheme().obtainStyledAttributes(new int[]{attr});
int resId = styledAttributes.getResourceId(0, 0);
styledAttributes.recycle();
if (resId != 0) {
return resId;
} else {
// work-around
styledAttributes = mContext.obtainStyledAttributes(new int[]{attr});
resId = styledAttributes.getResourceId(0, 0);
styledAttributes.recycle();
return resId;
}
}
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);