New MP4 muxer + Queue changes + Storage fixes

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

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

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

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

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

View File

@ -7,6 +7,7 @@ import android.preference.PreferenceManager;
import android.support.annotation.IdRes; import android.support.annotation.IdRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
@ -52,6 +53,7 @@ import icepick.State;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment"; private static final String TAG = "DialogFragment";
@ -263,7 +265,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Icepick.saveInstanceState(this, outState);
} }
@ -476,23 +478,40 @@ 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, (MissionCheck result) -> {
if (listed) { @StringRes int msgBtn;
@StringRes int msgBody;
switch (result) {
case Finished:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_warning;
break;
case Pending:
msgBtn = R.string.overwrite;
msgBody = R.string.download_already_pending;
break;
case PendingRunning:
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
default:
downloadSelected(context, stream, location, finalFileName, kind, threads);
return;
}
// overwrite or unique name actions are done by the download manager
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)
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running) .setMessage(msgBody)
.setPositiveButton( .setPositiveButton(
finished ? R.string.overwrite : R.string.generate_unique_name, msgBtn,
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
) )
.setNegativeButton(android.R.string.cancel, (dialog, which) -> { .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
dialog.cancel();
})
.create() .create()
.show(); .show();
} else {
downloadSelected(context, stream, location, finalFileName, kind, threads);
}
}); });
} }
@ -503,14 +522,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
String secondaryStreamUrl = null; String secondaryStreamUrl = null;
long nearLength = 0; long nearLength = 0;
if (selectedStream instanceof VideoStream) { if (selectedStream instanceof AudioStream) {
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
}
} else if (selectedStream instanceof VideoStream) {
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary() .getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondaryStream != null) { if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl(); secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null; psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);

View File

@ -17,7 +17,9 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"), REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"), REQUESTED_COMMENTS("requested comments"),
DELETE_FROM_HISTORY("delete from history"), DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"); PLAY_STREAM("Play stream"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed");
private final String message; private final String message;

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -12,6 +13,8 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
public class DownloadSettingsFragment extends BasePreferenceFragment { public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
@ -78,6 +81,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
defaultPreferences.edit().putString(key, path).apply(); defaultPreferences.edit().putString(key, path).apply();
updatePreferencesSummary(); updatePreferencesSummary();
File target = new File(path);
if (!target.canWrite()) {
AlertDialog.Builder msg = new AlertDialog.Builder(getContext());
msg.setTitle(R.string.download_to_sdcard_error_title);
msg.setMessage(R.string.download_to_sdcard_error_message);
msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { });
msg.show();
}
} }
} }
} }

View File

@ -1,9 +1,10 @@
package org.schabi.newpipe.streams; package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import org.schabi.newpipe.streams.io.SharpStream;
/** /**
* @author kapodamy * @author kapodamy
@ -15,89 +16,237 @@ public class DataReader {
public final static int INTEGER_SIZE = 4; public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4; public final static int FLOAT_SIZE = 4;
private long pos; private long position = 0;
public final SharpStream stream; private final SharpStream stream;
private final boolean rewind;
private InputStream view;
private int viewSize;
public DataReader(SharpStream stream) { public DataReader(SharpStream stream) {
this.rewind = stream.canRewind();
this.stream = stream; this.stream = stream;
this.pos = 0L; this.readOffset = this.readBuffer.length;
} }
public long position() { public long position() {
return pos; return position;
} }
public final int readInt() throws IOException { public int read() throws IOException {
if (fillBuffer()) {
return -1;
}
position++;
readCount--;
return readBuffer[readOffset++] & 0xFF;
}
public long skipBytes(long amount) throws IOException {
if (readCount < 0) {
return 0;
} else if (readCount == 0) {
amount = stream.skip(amount);
} else {
if (readCount > amount) {
readCount -= (int) amount;
readOffset += (int) amount;
} else {
amount = readCount + stream.skip(amount - readCount);
readCount = 0;
readOffset = readBuffer.length;
}
}
position += amount;
return amount;
}
public int readInt() throws IOException {
primitiveRead(INTEGER_SIZE); primitiveRead(INTEGER_SIZE);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
} }
public final int read() throws IOException { public short readShort() throws IOException {
int value = stream.read(); primitiveRead(SHORT_SIZE);
if (value == -1) { return (short) (primitive[0] << 8 | primitive[1]);
throw new EOFException();
} }
pos++; public long readLong() throws IOException {
return value;
}
public final long skipBytes(long amount) throws IOException {
amount = stream.skip(amount);
pos += amount;
return amount;
}
public final long readLong() throws IOException {
primitiveRead(LONG_SIZE); primitiveRead(LONG_SIZE);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low; return high << 32 | low;
} }
public final short readShort() throws IOException { public int read(byte[] buffer) throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length); return read(buffer, 0, buffer.length);
} }
public final int read(byte[] buffer, int offset, int count) throws IOException { public int read(byte[] buffer, int offset, int count) throws IOException {
int res = stream.read(buffer, offset, count); if (readCount < 0) {
pos += res; return -1;
}
int total = 0;
return res; if (count >= readBuffer.length) {
if (readCount > 0) {
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
readOffset += readCount;
offset += readCount;
count -= readCount;
total = readCount;
readCount = 0;
}
total += Math.max(stream.read(buffer, offset, count), 0);
} else {
while (count > 0 && !fillBuffer()) {
int read = Math.min(readCount, count);
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
readOffset += read;
readCount -= read;
offset += read;
count -= read;
total += read;
}
} }
public final boolean available() { position += total;
return stream.available() > 0; return total;
}
public boolean available() {
return readCount > 0 || stream.available() > 0;
} }
public void rewind() throws IOException { public void rewind() throws IOException {
stream.rewind(); stream.rewind();
pos = 0;
if ((position - viewSize) > 0) {
viewSize = 0;// drop view
} else {
viewSize += position;
}
position = 0;
readOffset = readBuffer.length;
} }
public boolean canRewind() { public boolean canRewind() {
return rewind; return stream.canRewind();
} }
private short[] primitive = new short[LONG_SIZE]; /**
* Wraps this instance of {@code DataReader} into {@code InputStream}
* object. Note: Any read in the {@code DataReader} will not modify
* (decrease) the view size
*
* @param size the size of the view
* @return the view
*/
public InputStream getView(int size) {
if (view == null) {
view = new InputStream() {
@Override
public int read() throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read();
if (res > 0) {
viewSize--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
viewSize -= res;
return res;
}
@Override
public long skip(long amount) throws IOException {
if (viewSize < 1) {
return 0;
}
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
viewSize -= res;
return res;
}
@Override
public int available() {
return viewSize;
}
@Override
public void close() {
viewSize = 0;
}
@Override
public boolean markSupported() {
return false;
}
};
}
viewSize = size;
return view;
}
private final short[] primitive = new short[LONG_SIZE];
private void primitiveRead(int amount) throws IOException { private void primitiveRead(int amount) throws IOException {
byte[] buffer = new byte[amount]; byte[] buffer = new byte[amount];
int read = stream.read(buffer, 0, amount); int read = read(buffer, 0, amount);
pos += read;
if (read != amount) { if (read != amount) {
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes"); throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
} }
for (int i = 0; i < buffer.length; i++) { for (int i = 0; i < amount; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
} }
} }
private final byte[] readBuffer = new byte[8 * 1024];
private int readOffset;
private int readCount;
private boolean fillBuffer() throws IOException {
if (readCount < 0) {
return true;
}
if (readOffset >= readBuffer.length) {
readCount = stream.read(readBuffer);
if (readCount < 1) {
readCount = -1;
return true;
}
readOffset = 0;
}
return readCount < 1;
}
} }

View File

@ -1,17 +1,15 @@
package org.schabi.newpipe.streams; package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import org.schabi.newpipe.streams.io.SharpStream;
/** /**
* @author kapodamy * @author kapodamy
*/ */
@ -35,14 +33,29 @@ public class Mp4DashReader {
private static final int ATOM_TREX = 0x74726578; private static final int ATOM_TREX = 0x74726578;
private static final int ATOM_TKHD = 0x746B6864; private static final int ATOM_TKHD = 0x746B6864;
private static final int ATOM_MFRA = 0x6D667261; private static final int ATOM_MFRA = 0x6D667261;
private static final int ATOM_TFRA = 0x74667261;
private static final int ATOM_MDHD = 0x6D646864; private static final int ATOM_MDHD = 0x6D646864;
private static final int ATOM_EDTS = 0x65647473;
private static final int ATOM_ELST = 0x656C7374;
private static final int ATOM_HDLR = 0x68646C72;
private static final int ATOM_MINF = 0x6D696E66;
private static final int ATOM_DINF = 0x64696E66;
private static final int ATOM_STBL = 0x7374626C;
private static final int ATOM_STSD = 0x73747364;
private static final int ATOM_VMHD = 0x766D6864;
private static final int ATOM_SMHD = 0x736D6864;
private static final int BRAND_DASH = 0x64617368; private static final int BRAND_DASH = 0x64617368;
private static final int BRAND_ISO5 = 0x69736F35;
private static final int HANDLER_VIDE = 0x76696465;
private static final int HANDLER_SOUN = 0x736F756E;
private static final int HANDLER_SUBT = 0x73756274;
// </editor-fold> // </editor-fold>
private final DataReader stream; private final DataReader stream;
private Mp4Track[] tracks = null; private Mp4Track[] tracks = null;
private int[] brands = null;
private Box box; private Box box;
private Moof moof; private Moof moof;
@ -50,9 +63,10 @@ public class Mp4DashReader {
private boolean chunkZero = false; private boolean chunkZero = false;
private int selectedTrack = -1; private int selectedTrack = -1;
private Box backupBox = null;
public enum TrackKind { public enum TrackKind {
Audio, Video, Other Audio, Video, Subtitles, Other
} }
public Mp4DashReader(SharpStream source) { public Mp4DashReader(SharpStream source) {
@ -65,8 +79,15 @@ public class Mp4DashReader {
} }
box = readBox(ATOM_FTYP); box = readBox(ATOM_FTYP);
if (parse_ftyp() != BRAND_DASH) { brands = parse_ftyp(box);
throw new NoSuchElementException("Main Brand is not dash"); switch (brands[0]) {
case BRAND_DASH:
case BRAND_ISO5:// ¿why not?
break;
default:
throw new NoSuchElementException(
"Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0])
);
} }
Moov moov = null; Moov moov = null;
@ -84,8 +105,6 @@ public class Mp4DashReader {
break; break;
case ATOM_MFRA: case ATOM_MFRA:
break; break;
case ATOM_MDAT:
throw new IOException("Expected moof, found mdat");
} }
} }
@ -107,15 +126,26 @@ public class Mp4DashReader {
} }
} }
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) { switch (moov.trak[i].mdia.hdlr.subType) {
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio; case HANDLER_VIDE:
} else {
tracks[i].kind = TrackKind.Video; tracks[i].kind = TrackKind.Video;
} break;
case HANDLER_SOUN:
tracks[i].kind = TrackKind.Audio;
break;
case HANDLER_SUBT:
tracks[i].kind = TrackKind.Subtitles;
break;
default:
tracks[i].kind = TrackKind.Other;
break;
} }
} }
public Mp4Track selectTrack(int index) { backupBox = box;
}
Mp4Track selectTrack(int index) {
selectedTrack = index; selectedTrack = index;
return tracks[index]; return tracks[index];
} }
@ -126,7 +156,7 @@ public class Mp4DashReader {
* @return list with a basic info * @return list with a basic info
* @throws IOException if the source stream is not seekeable * @throws IOException if the source stream is not seekeable
*/ */
public int getFragmentsCount() throws IOException { int getFragmentsCount() throws IOException {
if (selectedTrack < 0) { if (selectedTrack < 0) {
throw new IllegalStateException("track no selected"); throw new IllegalStateException("track no selected");
} }
@ -136,7 +166,6 @@ public class Mp4DashReader {
Box tmp; Box tmp;
int count = 0; int count = 0;
long orig_offset = stream.position();
if (box.type == ATOM_MOOF) { if (box.type == ATOM_MOOF) {
tmp = box; tmp = box;
@ -162,17 +191,36 @@ public class Mp4DashReader {
ensure(tmp); ensure(tmp);
} while (stream.available() && (tmp = readBox()) != null); } while (stream.available() && (tmp = readBox()) != null);
stream.rewind(); rewind();
stream.skipBytes((int) orig_offset);
return count; return count;
} }
public int[] getBrands() {
if (brands == null) throw new IllegalStateException("Not parsed");
return brands;
}
public void rewind() throws IOException {
if (!stream.canRewind()) {
throw new IOException("The provided stream doesn't allow seek");
}
if (box == null) {
return;
}
box = backupBox;
chunkZero = false;
stream.rewind();
stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2));
}
public Mp4Track[] getAvailableTracks() { public Mp4Track[] getAvailableTracks() {
return tracks; return tracks;
} }
public Mp4TrackChunk getNextChunk() throws IOException { public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
Mp4Track track = tracks[selectedTrack]; Mp4Track track = tracks[selectedTrack];
while (stream.available()) { while (stream.available()) {
@ -208,7 +256,7 @@ public class Mp4DashReader {
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount; moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
} else { } else {
moof.traf.trun.chunkSize = box.size - 8; moof.traf.trun.chunkSize = (int) (box.size - 8);
} }
} }
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) { if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
@ -228,9 +276,12 @@ public class Mp4DashReader {
continue;// find another chunk continue;// find another chunk
} }
Mp4TrackChunk chunk = new Mp4TrackChunk(); Mp4DashChunk chunk = new Mp4DashChunk();
chunk.moof = moof; chunk.moof = moof;
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize); if (!infoOnly) {
chunk.data = stream.getView(moof.traf.trun.chunkSize);
}
moof = null; moof = null;
stream.skipBytes(chunk.moof.traf.trun.dataOffset); stream.skipBytes(chunk.moof.traf.trun.dataOffset);
@ -269,6 +320,10 @@ public class Mp4DashReader {
b.size = stream.readInt(); b.size = stream.readInt();
b.type = stream.readInt(); b.type = stream.readInt();
if (b.size == 1) {
b.size = stream.readLong();
}
return b; return b;
} }
@ -280,6 +335,25 @@ public class Mp4DashReader {
return b; return b;
} }
private byte[] readFullBox(Box ref) throws IOException {
// full box reading is limited to 2 GiB, and should be enough
int size = (int) ref.size;
ByteBuffer buffer = ByteBuffer.allocate(size);
buffer.putInt(size);
buffer.putInt(ref.type);
int read = size - 8;
if (stream.read(buffer.array(), 8, read) != read) {
throw new EOFException(
String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size)
);
}
return buffer.array();
}
private void ensure(Box ref) throws IOException { private void ensure(Box ref) throws IOException {
long skip = ref.offset + ref.size - stream.position(); long skip = ref.offset + ref.size - stream.position();
@ -310,6 +384,14 @@ public class Mp4DashReader {
return null; return null;
} }
private Box untilAnyBox(Box ref) throws IOException {
if (stream.position() >= (ref.offset + ref.size)) {
return null;
}
return readBox();
}
// </editor-fold> // </editor-fold>
// <editor-fold defaultState="collapsed" desc="Box readers"> // <editor-fold defaultState="collapsed" desc="Box readers">
@ -448,11 +530,18 @@ public class Mp4DashReader {
return obj; return obj;
} }
private int parse_ftyp() throws IOException { private int[] parse_ftyp(Box ref) throws IOException {
int brand = stream.readInt(); int i = 0;
int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)];
list[i++] = stream.readInt();// major brand
stream.skipBytes(4);// minor version stream.skipBytes(4);// minor version
return brand; for (; i < list.length; i++)
list[i] = stream.readInt();// compatible brands
return list;
} }
private Mvhd parse_mvhd() throws IOException { private Mvhd parse_mvhd() throws IOException {
@ -521,32 +610,66 @@ public class Mp4DashReader {
trak.tkhd = parse_tkhd(); trak.tkhd = parse_tkhd();
ensure(b); ensure(b);
b = untilBox(ref, ATOM_MDIA); while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
trak.mdia = new byte[b.size]; switch (b.type) {
case ATOM_MDIA:
trak.mdia = parse_mdia(b);
break;
case ATOM_EDTS:
trak.edst_elst = parse_edts(b);
break;
}
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia); ensure(b);
buffer.putInt(b.size); }
buffer.putInt(ATOM_MDIA);
stream.read(trak.mdia, 8, b.size - 8);
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
return trak; return trak;
} }
private int parse_mdia(ByteBuffer data) { private Mdia parse_mdia(Box ref) throws IOException {
while (data.hasRemaining()) { Mdia obj = new Mdia();
int end = data.position() + data.getInt();
if (data.getInt() == ATOM_MDHD) { Box b;
byte version = data.get(); while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2)); switch (b.type) {
return data.getInt(); case ATOM_MDHD:
obj.mdhd = readFullBox(b);
// read time scale
ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd);
byte version = buffer.get(8);
buffer.position(12 + ((version == 0 ? 4 : 8) * 2));
obj.mdhd_timeScale = buffer.getInt();
break;
case ATOM_HDLR:
obj.hdlr = parse_hdlr(b);
break;
case ATOM_MINF:
obj.minf = parse_minf(b);
break;
}
ensure(b);
} }
data.position(end); return obj;
} }
return 0;// this NEVER should happen private Hdlr parse_hdlr(Box ref) throws IOException {
// version
// flags
stream.skipBytes(4);
Hdlr obj = new Hdlr();
obj.bReserved = new byte[12];
obj.type = stream.readInt();
obj.subType = stream.readInt();
stream.read(obj.bReserved);
// component name (is a ansi/ascii string)
stream.skipBytes((ref.offset + ref.size) - stream.position());
return obj;
} }
private Moov parse_moov(Box ref) throws IOException { private Moov parse_moov(Box ref) throws IOException {
@ -570,7 +693,7 @@ public class Mp4DashReader {
ensure(b); ensure(b);
} }
moov.trak = tmp.toArray(new Trak[tmp.size()]); moov.trak = tmp.toArray(new Trak[0]);
return moov; return moov;
} }
@ -584,7 +707,7 @@ public class Mp4DashReader {
ensure(b); ensure(b);
} }
return tmp.toArray(new Trex[tmp.size()]); return tmp.toArray(new Trex[0]);
} }
private Trex parse_trex() throws IOException { private Trex parse_trex() throws IOException {
@ -602,74 +725,74 @@ public class Mp4DashReader {
return obj; return obj;
} }
private Tfra parse_tfra() throws IOException { private Elst parse_edts(Box ref) throws IOException {
int version = stream.read(); Box b = untilBox(ref, ATOM_ELST);
if (b == null) {
stream.skipBytes(3);// flags return null;
Tfra tfra = new Tfra();
tfra.trackId = stream.readInt();
stream.skipBytes(3);// reserved
int bFlags = stream.read();
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
tfra.entries_time = new int[stream.readInt()];
for (int i = 0; i < tfra.entries_time.length; i++) {
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
} }
return tfra; Elst obj = new Elst();
}
private Sidx parse_sidx() throws IOException {
int version = stream.read();
boolean v1 = stream.read() == 1;
stream.skipBytes(3);// flags stream.skipBytes(3);// flags
Sidx obj = new Sidx(); int entryCount = stream.readInt();
obj.referenceId = stream.readInt(); if (entryCount < 1) {
obj.timescale = stream.readInt(); obj.bMediaRate = 0x00010000;// default media rate (1.0)
return obj;
}
// earliest presentation entries_time if (v1) {
// first offset stream.skipBytes(DataReader.LONG_SIZE);// segment duration
// reserved obj.MediaTime = stream.readLong();
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2); // ignore all remain entries
stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2));
} else {
stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration
obj.MediaTime = stream.readInt();
}
obj.entries_subsegmentDuration = new int[stream.readShort()]; obj.bMediaRate = stream.readInt();
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) { return obj;
// reference type }
// referenced size
stream.skipBytes(4);
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
// starts with SAP private Minf parse_minf(Box ref) throws IOException {
// SAP type Minf obj = new Minf();
// SAP delta entries_time
stream.skipBytes(4); Box b;
while ((b = untilAnyBox(ref)) != null) {
switch (b.type) {
case ATOM_DINF:
obj.dinf = readFullBox(b);
break;
case ATOM_STBL:
obj.stbl_stsd = parse_stbl(b);
break;
case ATOM_VMHD:
case ATOM_SMHD:
obj.$mhd = readFullBox(b);
break;
}
ensure(b);
} }
return obj; return obj;
} }
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException { /**
ArrayList<Tfra> tmp = new ArrayList<>(trackCount); * this only read the "stsd" box inside
long limit = ref.offset + ref.size; */
private byte[] parse_stbl(Box ref) throws IOException {
Box b = untilBox(ref, ATOM_STSD);
while (stream.position() < limit) { if (b == null) {
box = readBox(); return new byte[0];// this never should happens (missing codec startup data)
if (box.type == ATOM_TFRA) {
tmp.add(parse_tfra());
} }
ensure(box); return readFullBox(b);
}
return tmp.toArray(new Tfra[tmp.size()]);
} }
// </editor-fold> // </editor-fold>
@ -679,14 +802,7 @@ public class Mp4DashReader {
int type; int type;
long offset; long offset;
int size; long size;
}
class Sidx {
int timescale;
int referenceId;
int[] entries_subsegmentDuration;
} }
public class Moof { public class Moof {
@ -711,12 +827,16 @@ public class Mp4DashReader {
int defaultSampleFlags; int defaultSampleFlags;
} }
public class TrunEntry { class TrunEntry {
int sampleDuration;
int sampleSize;
int sampleFlags;
int sampleCompositionTimeOffset;
boolean hasCompositionTimeOffset;
boolean isKeyframe;
public int sampleDuration;
public int sampleSize;
public int sampleFlags;
public int sampleCompositionTimeOffset;
} }
public class Trun { public class Trun {
@ -749,6 +869,31 @@ public class Mp4DashReader {
entry.sampleCompositionTimeOffset = buffer.getInt(); entry.sampleCompositionTimeOffset = buffer.getInt();
} }
entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800);
entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000);
return entry;
}
public TrunEntry getAbsoluteEntry(int i, Tfhd header) {
TrunEntry entry = getEntry(i);
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) {
entry.sampleFlags = header.defaultSampleFlags;
}
if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) {
entry.sampleSize = header.defaultSampleSize;
}
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) {
entry.sampleDuration = header.defaultSampleDuration;
}
if (i == 0 && hasFlag(bFlags, 0x0004)) {
entry.sampleFlags = bFirstSampleFlags;
}
return entry; return entry;
} }
} }
@ -768,9 +913,9 @@ public class Mp4DashReader {
public class Trak { public class Trak {
public Tkhd tkhd; public Tkhd tkhd;
public int mdia_mdhd_timeScale; public Elst edst_elst;
public Mdia mdia;
byte[] mdia;
} }
class Mvhd { class Mvhd {
@ -786,12 +931,6 @@ public class Mp4DashReader {
Trex[] mvex_trex; Trex[] mvex_trex;
} }
class Tfra {
int trackId;
int[] entries_time;
}
public class Trex { public class Trex {
private int trackId; private int trackId;
@ -801,6 +940,34 @@ public class Mp4DashReader {
int defaultSampleFlags; int defaultSampleFlags;
} }
public class Elst {
public long MediaTime;
public int bMediaRate;
}
public class Mdia {
public int mdhd_timeScale;
public byte[] mdhd;
public Hdlr hdlr;
public Minf minf;
}
public class Hdlr {
public int type;
public int subType;
public byte[] bReserved;
}
public class Minf {
public byte[] dinf;
public byte[] stbl_stsd;
public byte[] $mhd;
}
public class Mp4Track { public class Mp4Track {
public TrackKind kind; public TrackKind kind;
@ -808,10 +975,43 @@ public class Mp4DashReader {
public Trex trex; public Trex trex;
} }
public class Mp4TrackChunk { public class Mp4DashChunk {
public InputStream data; public InputStream data;
public Moof moof; public Moof moof;
private int i = 0;
public TrunEntry getNextSampleInfo() {
if (i >= moof.traf.trun.entryCount) {
return null;
}
return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
}
public Mp4DashSample getNextSample() throws IOException {
if (data == null) {
throw new IllegalStateException("This chunk has info only");
}
if (i >= moof.traf.trun.entryCount) {
return null;
}
Mp4DashSample sample = new Mp4DashSample();
sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
sample.data = new byte[sample.info.sampleSize];
if (data.read(sample.data) != sample.info.sampleSize) {
throw new EOFException("EOF reached while reading a sample");
}
return sample;
}
}
public class Mp4DashSample {
public TrunEntry info;
public byte[] data;
} }
//</editor-fold> //</editor-fold>
} }

View File

@ -1,623 +0,0 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
import org.schabi.newpipe.streams.Mp4DashReader.Trak;
import org.schabi.newpipe.streams.Mp4DashReader.Trex;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
/**
*
* @author kapodamy
*/
public class Mp4DashWriter {
private final static byte DIMENSIONAL_FIVE = 5;
private final static byte DIMENSIONAL_TWO = 2;
private final static short DEFAULT_TIMESCALE = 1000;
private final static int BUFFER_SIZE = 8 * 1024;
private final static byte DEFAULT_TREX_SIZE = 32;
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
private final static int EPOCH_OFFSET = 2082844800;
private Mp4Track[] infoTracks;
private SharpStream[] sourceTracks;
private Mp4DashReader[] readers;
private final long time;
private boolean done = false;
private boolean parsed = false;
private long written = 0;
private ArrayList<ArrayList<Integer>> chunkTimes;
private ArrayList<Long> moofOffsets;
private ArrayList<Integer> fragSizes;
public Mp4DashWriter(SharpStream... source) {
sourceTracks = source;
readers = new Mp4DashReader[sourceTracks.length];
infoTracks = new Mp4Track[sourceTracks.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new Mp4DashReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
if (chunkTimes != null) {
throw new IOException("tracks already selected");
}
try {
chunkTimes = new ArrayList<>(readers.length);
moofOffsets = new ArrayList<>(32);
fragSizes = new ArrayList<>(32);
for (int i = 0; i < readers.length; i++) {
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
chunkTimes.add(new ArrayList<Integer>(32));
}
} finally {
parsed = true;
}
}
public long getBytesWritten() {
return written;
}
public void build(SharpStream out) throws IOException, RuntimeException {
if (done) {
throw new RuntimeException("already done");
}
if (!out.canWrite()) {
throw new IOException("the provided output is not writable");
}
long sidxOffsets = -1;
int maxFrags = 0;
for (SharpStream stream : sourceTracks) {
if (!stream.canRewind()) {
sidxOffsets = -2;// sidx not available
}
}
try {
dump(make_ftyp(), out);
dump(make_moov(), out);
if (sidxOffsets == -1 && out.canRewind()) {
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
int reserved = 0;
for (Mp4DashReader reader : readers) {
int count = reader.getFragmentsCount();
if (count > maxFrags) {
maxFrags = count;
}
reserved += 12 + calcSidxBodySize(count);
}
if (maxFrags > 0xFFFF) {
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
} else {
sidxOffsets = written;
dump(make_free(reserved), out);
}
//</editor-fold>
}
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
chunks.add(null);
int read;
byte[] buffer = new byte[BUFFER_SIZE];
int sequenceNumber = 1;
while (true) {
chunks.clear();
for (int i = 0; i < readers.length; i++) {
Mp4TrackChunk chunk = readers[i].getNextChunk();
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
continue;
}
chunk.moof.traf.tfhd.trackId = i + 1;
chunks.add(chunk);
if (sequenceNumber == 1) {
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
} else {
chunkTimes.get(i).add(0);
}
}
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
}
if (chunks.size() < 1) {
break;
}
long offset = written;
moofOffsets.add(offset);
dump(make_moof(sequenceNumber++, chunks, offset), out);
dump(make_mdat(chunks), out);
for (Mp4TrackChunk chunk : chunks) {
while ((read = chunk.data.read(buffer)) > 0) {
out.write(buffer, 0, read);
written += read;
}
}
fragSizes.add((int) (written - offset));
}
dump(make_mfra(), out);
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
long len = written;
out.rewind();
out.skip(sidxOffsets);
written = sidxOffsets;
sidxOffsets = moofOffsets.get(0);
for (int i = 0; i < readers.length; i++) {
dump(make_sidx(i, sidxOffsets - written), out);
}
written = len;
}
} finally {
done = true;
}
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
}
sourceTracks = null;
readers = null;
infoTracks = null;
moofOffsets = null;
chunkTimes = null;
}
// <editor-fold defaultstate="collapsed" desc="Utils">
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
for (byte[] buff : buffer) {
stream.write(buff);
written += buff.length;
}
}
private byte[][] lengthFor(byte[][] buffer) {
int length = 0;
for (byte[] buff : buffer) {
length += buff.length;
}
ByteBuffer.wrap(buffer[0]).putInt(length);
return buffer;
}
private int calcSidxBodySize(int entryCount) {
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="Box makers">
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
int pos = 2;
TrunExtra[] extra = new TrunExtra[chunks.size()];
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
};
buffer[1] = new byte[4];
ByteBuffer.wrap(buffer[1]).putInt(sequence);
for (int i = 0; i < extra.length; i++) {
extra[i] = new TrunExtra();
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
buffer[pos++] = buff;
}
}
lengthFor(buffer);
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
for (int i = 0; i < extra.length; i++) {
extra[i].byteBuffer.putInt(offset);
offset += chunks.get(i).moof.traf.trun.chunkSize;
}
return buffer;
}
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
};
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
byte tfhdBodySize = 8 + 8;
if (hasFlag(flags, 0x08)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x10)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x20)) {
tfhdBodySize += 4;
}
buffer[1] = new byte[tfhdBodySize];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.position(4);
set.putInt(chunk.moof.traf.tfhd.trackId);
set.putLong(moofOffset);
if (hasFlag(flags, 0x08)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
}
if (hasFlag(flags, 0x10)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
}
if (hasFlag(flags, 0x20)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
}
set.putInt(0, flags);
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
buffer[2] = new byte[]{
0x00, 0x00, 0x00, 0x14,
0x74, 0x66, 0x64, 0x74,
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
buffer[3] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
buffer[4] = chunk.moof.traf.trun.bEntries;
lengthFor(buffer);
set = ByteBuffer.wrap(buffer[3]);
set.putInt(buffer[3].length + buffer[4].length);
set.position(8);
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
set.putInt(chunk.moof.traf.trun.entryCount);
extra.byteBuffer = set;
return buffer;
}
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
}
};
int length = 0;
for (Mp4TrackChunk chunk : chunks) {
length += chunk.moof.traf.trun.chunkSize;
}
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
return buffer;
}
private byte[][] make_ftyp() {
return new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
}
};
}
private byte[][] make_mvhd() {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[28];
buffer[2] = new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
};
buffer[3] = new byte[24];// predefined
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
long longestTrack = 0;
for (Mp4Track track : infoTracks) {
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
if (tmp > longestTrack) {
longestTrack = tmp;
}
}
ByteBuffer.wrap(buffer[1])
.putLong(time)
.putLong(time)
.putInt(DEFAULT_TIMESCALE)
.putLong(longestTrack);
return buffer;
}
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
if (trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36)");
}
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
};
buffer[1] = new byte[48];
buffer[2] = trak.tkhd.matrix;
buffer[3] = new byte[8];
buffer[4] = trak.mdia;
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putLong(time);
set.putLong(time);
set.putInt(trackId);
set.position(24);
set.putLong(trak.tkhd.duration);
set.position(40);
set.putShort(trak.tkhd.bLayer);
set.putShort(trak.tkhd.bAlternateGroup);
set.putShort(trak.tkhd.bVolume);
ByteBuffer.wrap(buffer[3])
.putInt(trak.tkhd.bWidth)
.putInt(trak.tkhd.bHeight);
return lengthFor(buffer);
}
private byte[][] make_moov() throws RuntimeException {
int pos = 1;
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
};
for (byte[] buff : make_mvhd()) {
buffer[pos++] = buff;
}
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
};
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
buffer[pos++] = buff;
}
}
// default udta
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
};
return lengthFor(buffer);
}
private byte[][] make_trex(int trackId, Trex trex) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
},
new byte[20]
};
ByteBuffer.wrap(buffer[1])
.putInt(trackId)
.putInt(trex.defaultSampleDescriptionIndex)
.putInt(trex.defaultSampleDuration)
.putInt(trex.defaultSampleSize)
.putInt(trex.defaultSampleFlags);
return buffer;
}
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
int entryCount = times.size() - 1;
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(trackId);
set.position(8);
set.putInt(entryCount);
long decodeTime = 0;
for (int i = 0; i < entryCount; i++) {
decodeTime += times.get(i);
set.putLong(decodeTime);
set.putLong(moofOffsets.get(i));
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
}
return lengthFor(buffer);
}
private byte[][] make_mfra() {
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
};
int pos = 1;
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{// mfro
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
set.position(12);
set.put(buffer[0], 0, 4);
return buffer;
}
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
List<Integer> times = chunkTimes.get(internalTrackId);
int count = times.size() - 1;// the first item is ignored (composition time)
if (count > 65535) {
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
}
byte[][] buffer = new byte[][]{
new byte[]{
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
},
new byte[calcSidxBodySize(count)]
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(internalTrackId + 1);
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
set.putLong(0);
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
set.putInt(0xFFFF & count);// unsigned
int i = 0;
while (i < count) {
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
set.putInt(times.get(i + 1));
set.putInt(0x90000000);// default SAP settings
i++;
}
return buffer;
}
private byte[][] make_free(int totalSize) {
return lengthFor(new byte[][]{
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
new byte[totalSize - 8]// this is waste of RAM
});
}
//</editor-fold>
class TrunExtra {
ByteBuffer byteBuffer;
}
}

View File

@ -0,0 +1,810 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
*
* @author kapodamy
*/
public class Mp4FromDashWriter {
private final static int EPOCH_OFFSET = 2082844800;
private final static short DEFAULT_TIMESCALE = 1000;
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
private final long time;
private ByteBuffer auxBuffer;
private SharpStream outStream;
private long lastWriteOffset = -1;
private long writeOffset;
private boolean moovSimulation = true;
private boolean done = false;
private boolean parsed = false;
private Mp4Track[] tracks;
private SharpStream[] sourceTracks;
private Mp4DashReader[] readers;
private Mp4DashChunk[] readersChunks;
private int overrideMainBrand = 0x00;
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
for (SharpStream src : sources) {
if (!src.canRewind() && !src.canRead()) {
throw new IOException("All sources must be readable and allow rewind");
}
}
sourceTracks = sources;
readers = new Mp4DashReader[sourceTracks.length];
readersChunks = new Mp4DashChunk[readers.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new Mp4DashReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
if (tracks != null) {
throw new IOException("tracks already selected");
}
try {
tracks = new Mp4Track[readers.length];
for (int i = 0; i < readers.length; i++) {
tracks[i] = readers[i].selectTrack(trackIndex[i]);
}
} finally {
parsed = true;
}
}
public void setMainBrand(int brandId) {
overrideMainBrand = brandId;
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() throws IOException {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
}
tracks = null;
sourceTracks = null;
readers = null;
readersChunks = null;
auxBuffer = null;
outStream = null;
}
public void build(SharpStream output) throws IOException {
if (done) {
throw new RuntimeException("already done");
}
if (!output.canWrite()) {
throw new IOException("the provided output is not writable");
}
//
// WARNING: the muxer requires at least 8 samples of every track
// not allowed for very short tracks (less than 0.5 seconds)
//
outStream = output;
int read = 8;// mdat box header size
long totalSampleSize = 0;
int[] sampleExtra = new int[readers.length];
int[] defaultMediaTime = new int[readers.length];
int[] defaultSampleDuration = new int[readers.length];
int[] sampleCount = new int[readers.length];
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
for (int i = 0; i < tablesInfo.length; i++) {
tablesInfo[i] = new TablesInfo();
}
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
for (int i = 0; i < readers.length; i++) {
int samplesSize = 0;
int sampleSizeChanges = 0;
int compositionOffsetLast = -1;
Mp4DashChunk chunk;
while ((chunk = readers[i].getNextChunk(true)) != null) {
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
}
read += chunk.moof.traf.trun.chunkSize;
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
TrunEntry info;
while ((info = chunk.getNextSampleInfo()) != null) {
if (info.isKeyframe) {
tablesInfo[i].stss++;
}
if (info.sampleDuration > defaultSampleDuration[i]) {
defaultSampleDuration[i] = info.sampleDuration;
}
tablesInfo[i].stsz++;
if (samplesSize != info.sampleSize) {
samplesSize = info.sampleSize;
sampleSizeChanges++;
}
if (info.hasCompositionTimeOffset) {
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
tablesInfo[i].ctts++;
compositionOffsetLast = info.sampleCompositionTimeOffset;
}
}
totalSampleSize += info.sampleSize;
}
}
if (defaultMediaTime[i] < 1) {
defaultMediaTime[i] = defaultSampleDuration[i];
}
readers[i].rewind();
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
tmp = tmp % SAMPLES_PER_CHUNK;
if (tmp == 0) {
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1
};
} else {
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1,
tablesInfo[i].stco + 1, tmp, 1
};
tablesInfo[i].stco++;
}
sampleCount[i] = tablesInfo[i].stsz;
if (sampleSizeChanges == 1) {
tablesInfo[i].stsz = 0;
tablesInfo[i].stsz_default = samplesSize;
} else {
tablesInfo[i].stsz_default = 0;
}
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
}
// ensure track duration
if (tracks[i].trak.tkhd.duration < 1) {
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
}
}
//</editor-fold>
boolean is64 = read > THRESHOLD_FOR_CO64;
// calculate the moov size;
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
if (auxSize < THRESHOLD_MOOV_LENGTH) {
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
}
moovSimulation = false;
writeOffset = 0;
final int ftyp_size = make_ftyp();
// reserve moov space in the output stream
if (outStream.canSetLength()) {
long length = writeOffset + auxSize;
outStream.setLength(length);
outSeek(length);
} else {
// hard way
int length = auxSize;
byte[] buffer = new byte[8 * 1024];// 8 KiB
while (length > 0) {
int count = Math.min(length, buffer.length);
outWrite(buffer, 0, count);
length -= count;
}
}
if (auxBuffer == null) {
outSeek(ftyp_size);
}
// tablesInfo contais row counts
// and after returning from make_moov() will contain table offsets
make_moov(defaultMediaTime, tablesInfo, is64);
// write tables: stts stsc
// reset for ctts table: sampleCount sampleExtra
for (int i = 0; i < readers.length; i++) {
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
tablesInfo[i].stsc_bEntries = null;
if (tablesInfo[i].ctts > 0) {
sampleCount[i] = 1;// index is not base zero
sampleExtra[i] = -1;
}
}
if (auxBuffer == null) {
outRestore();
}
outWrite(make_mdat(totalSampleSize, is64));
int[] sampleIndex = new int[readers.length];
int[] sizes = new int[SAMPLES_PER_CHUNK];
int[] sync = new int[SAMPLES_PER_CHUNK];
int written = readers.length;
while (written > 0) {
written = 0;
for (int i = 0; i < readers.length; i++) {
if (sampleIndex[i] < 0) {
continue;// track is done
}
long chunkOffset = writeOffset;
int syncCount = 0;
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
int j = 0;
for (; j < limit; j++) {
Mp4DashSample sample = getNextSample(i);
if (sample == null) {
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
}
sampleIndex[i] = -1;
break;
}
sampleIndex[i]++;
if (tablesInfo[i].ctts > 0) {
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
sampleCount[i]++;
} else {
if (sampleExtra[i] >= 0) {
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
outRestore();
}
sampleCount[i] = 1;
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
}
}
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
sync[syncCount++] = sampleIndex[i];
}
if (tablesInfo[i].stsz > 0) {
sizes[j] = sample.data.length;
}
outWrite(sample.data, 0, sample.data.length);
}
if (j > 0) {
written++;
if (tablesInfo[i].stsz > 0) {
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
}
if (syncCount > 0) {
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
}
if (is64) {
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
} else {
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
}
outRestore();
}
}
}
if (auxBuffer != null) {
// dump moov
outSeek(ftyp_size);
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
auxBuffer = null;
}
}
private Mp4DashSample getNextSample(int track) throws IOException {
if (readersChunks[track] == null) {
readersChunks[track] = readers[track].getNextChunk(false);
if (readersChunks[track] == null) {
return null;// EOF reached
}
}
Mp4DashSample sample = readersChunks[track].getNextSample();
if (sample == null) {
readersChunks[track] = null;
return getNextSample(track);
} else {
return sample;
}
}
// <editor-fold defaultstate="expanded" desc="Stbl handling">
private int writeEntry64(int offset, long value) throws IOException {
outBackup();
auxSeek(offset);
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
return offset + 8;
}
private int writeEntryArray(int offset, int count, int... values) throws IOException {
outBackup();
auxSeek(offset);
int size = count * 4;
ByteBuffer buffer = ByteBuffer.allocate(size);
for (int i = 0; i < count; i++) {
buffer.putInt(values[i]);
}
auxWrite(buffer.array());
return offset + size;
}
private void outBackup() {
if (auxBuffer == null && lastWriteOffset < 0) {
lastWriteOffset = writeOffset;
}
}
/**
* Restore to the previous position before the first call to writeEntry64()
* or writeEntryArray() methods.
*/
private void outRestore() throws IOException {
if (lastWriteOffset > 0) {
outSeek(lastWriteOffset);
lastWriteOffset = -1;
}
}
// </editor-fold>
// <editor-fold defaultstate="expanded" desc="Utils">
private void outWrite(byte[] buffer) throws IOException {
outWrite(buffer, 0, buffer.length);
}
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
writeOffset += count;
outStream.write(buffer, offset, count);
}
private void outSeek(long offset) throws IOException {
if (outStream.canSeek()) {
outStream.seek(offset);
writeOffset = offset;
} else if (outStream.canRewind()) {
outStream.rewind();
writeOffset = 0;
outSkip(offset);
} else {
throw new IOException("cannot seek or rewind the output stream");
}
}
private void outSkip(long amount) throws IOException {
outStream.skip(amount);
writeOffset += amount;
}
private int lengthFor(int offset) throws IOException {
int size = auxOffset() - offset;
if (moovSimulation) {
return size;
}
auxSeek(offset);
auxWrite(size);
auxSkip(size - 4);
return size;
}
private int make(int type, int extra, int columns, int rows) throws IOException {
final byte base = 16;
int size = columns * rows * 4;
int total = size + base;
int offset = auxOffset();
if (extra >= 0) {
total += 4;
}
auxWrite(ByteBuffer.allocate(12)
.putInt(total)
.putInt(type)
.putInt(0x00)// default version & flags
.array()
);
if (extra >= 0) {
//size += 4;// commented for auxiliar buffer !!!
offset += 4;
auxWrite(extra);
}
auxWrite(rows);
auxSkip(size);
return offset + base;
}
private void auxWrite(int value) throws IOException {
auxWrite(ByteBuffer.allocate(4)
.putInt(value)
.array()
);
}
private void auxWrite(byte[] buffer) throws IOException {
if (moovSimulation) {
writeOffset += buffer.length;
} else if (auxBuffer == null) {
outWrite(buffer, 0, buffer.length);
} else {
auxBuffer.put(buffer);
}
}
private void auxSeek(int offset) throws IOException {
if (moovSimulation) {
writeOffset = offset;
} else if (auxBuffer == null) {
outSeek(offset);
} else {
auxBuffer.position(offset);
}
}
private void auxSkip(int amount) throws IOException {
if (moovSimulation) {
writeOffset += amount;
} else if (auxBuffer == null) {
outSkip(amount);
} else {
auxBuffer.position(auxBuffer.position() + amount);
}
}
private int auxOffset() {
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
}
// </editor-fold>
// <editor-fold defaultstate="expanded" desc="Box makers">
private int make_ftyp() throws IOException {
byte[] buffer = new byte[]{
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
0x00, 0x00, 0x02, 0x00,// default minor version (512)
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
};
if (overrideMainBrand != 0)
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
outWrite(buffer);
return buffer.length;
}
private byte[] make_mdat(long refSize, boolean is64) {
if (is64) {
refSize += 16;
} else {
refSize += 8;
}
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
.putInt(is64 ? 0x01 : (int) refSize)
.putInt(0x6D646174);// mdat
if (is64) {
buffer.putLong(refSize);
}
return buffer.array();
}
private void make_mvhd(long longestTrack) throws IOException {
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
});
auxWrite(ByteBuffer.allocate(28)
.putLong(time)
.putLong(time)
.putInt(DEFAULT_TIMESCALE)
.putLong(longestTrack)
.array()
);
auxWrite(new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
});
auxWrite(new byte[24]);// predefined
auxWrite(ByteBuffer.allocate(4)
.putInt(tracks.length + 1)
.array()
);
}
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
int start = auxOffset();
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
});
long longestTrack = 0;
long[] durations = new long[tracks.length];
for (int i = 0; i < durations.length; i++) {
durations[i] = (long) Math.ceil(
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
);
if (durations[i] > longestTrack) {
longestTrack = durations[i];
}
}
make_mvhd(longestTrack);
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
}
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
}
// udta/meta/ilst/©too
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
});
return lengthFor(start);
}
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
int start = auxOffset();
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
});
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.putLong(time);
buffer.putLong(time);
buffer.putInt(index + 1);
buffer.position(24);
buffer.putLong(duration);
buffer.position(40);
buffer.putShort(tracks[index].trak.tkhd.bLayer);
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
buffer.putShort(tracks[index].trak.tkhd.bVolume);
auxWrite(buffer.array());
auxWrite(tracks[index].trak.tkhd.matrix);
auxWrite(ByteBuffer.allocate(8)
.putInt(tracks[index].trak.tkhd.bWidth)
.putInt(tracks[index].trak.tkhd.bHeight)
.array()
);
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
});
int bMediaRate;
int mediaTime;
if (tracks[index].trak.edst_elst == null) {
// is a audio track ¿is edst/elst opcional for audio tracks?
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
bMediaRate = 0x00010000;
} else {
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
}
auxWrite(ByteBuffer
.allocate(12)
.putInt((int) duration)
.putInt(mediaTime)
.putInt(bMediaRate)
.array()
);
make_mdia(tracks[index].trak.mdia, tables, is64);
lengthFor(start);
}
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
int start_mdia = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
auxWrite(mdia.mdhd);
auxWrite(make_hdlr(mdia.hdlr));
int start_minf = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
auxWrite(mdia.minf.$mhd);
auxWrite(mdia.minf.dinf);
int start_stbl = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
auxWrite(mdia.minf.stbl_stsd);
//
// In audio tracks the following tables is not required: ssts ctts
// And stsz can be empty if has a default sample size
//
if (moovSimulation) {
make(0x73747473, -1, 2, 1);
if (tablesInfo.stss > 0) {
make(0x73747373, -1, 1, tablesInfo.stss);
}
if (tablesInfo.ctts > 0) {
make(0x63747473, -1, 2, tablesInfo.ctts);
}
make(0x73747363, -1, 3, tablesInfo.stsc);
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
} else {
tablesInfo.stts = make(0x73747473, -1, 2, 1);
if (tablesInfo.stss > 0) {
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
}
if (tablesInfo.ctts > 0) {
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
}
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
}
lengthFor(start_stbl);
lengthFor(start_minf);
lengthFor(start_mdia);
}
private byte[] make_hdlr(Hdlr hdlr) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
});
buffer.position(12);
buffer.putInt(hdlr.type);
buffer.putInt(hdlr.subType);
buffer.put(hdlr.bReserved);// always is a zero array
return buffer.array();
}
//</editor-fold>
class TablesInfo {
public int stts;
public int stsc;
public int[] stsc_bEntries;
public int ctts;
public int stsz;
public int stsz_default;
public int stss;
public int stco;
}
}

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.streams; package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
@ -12,8 +13,6 @@ import java.nio.charset.Charset;
import java.text.ParseException; import java.text.ParseException;
import java.util.Locale; import java.util.Locale;
import org.schabi.newpipe.streams.io.SharpStream;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
@ -70,7 +69,7 @@ public class SubtitleConverter {
* Language parsing is not supported * Language parsing is not supported
*/ */
byte[] buffer = new byte[source.available()]; byte[] buffer = new byte[(int) source.available()];
source.read(buffer); source.read(buffer);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
@ -206,7 +205,7 @@ public class SubtitleConverter {
} }
} }
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException { private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
Element ref = xml.getDocumentElement(); Element ref = xml.getDocumentElement();
for (int i = 0; i < path.length - 1; i++) { for (int i = 0; i < path.length - 1; i++) {

View File

@ -1,65 +0,0 @@
package org.schabi.newpipe.streams;
import java.io.InputStream;
import java.io.IOException;
public class TrackDataChunk extends InputStream {
private final DataReader base;
private int size;
public TrackDataChunk(DataReader base, int size) {
this.base = base;
this.size = size;
}
@Override
public int read() throws IOException {
if (size < 1) {
return -1;
}
int res = base.read();
if (res >= 0) {
size--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
count = Math.min(size, count);
int read = base.read(buffer, offset, count);
size -= count;
return read;
}
@Override
public long skip(long amount) throws IOException {
long res = base.skipBytes(Math.min(amount, size));
size -= res;
return res;
}
@Override
public int available() {
return size;
}
@Override
public void close() {
size = 0;
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@ -1,12 +1,13 @@
package org.schabi.newpipe.streams; package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects;
import org.schabi.newpipe.streams.io.SharpStream;
/** /**
* *
@ -121,7 +122,7 @@ public class WebMReader {
} }
private String readString(Element parent) throws IOException { private String readString(Element parent) throws IOException {
return new String(readBlob(parent), "utf-8"); return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8"
} }
private byte[] readBlob(Element parent) throws IOException { private byte[] readBlob(Element parent) throws IOException {
@ -193,6 +194,7 @@ public class WebMReader {
return elem; return elem;
} }
} }
ensure(elem); ensure(elem);
} }
@ -306,7 +308,7 @@ public class WebMReader {
entry.trackNumber = readNumber(elem); entry.trackNumber = readNumber(elem);
break; break;
case ID_TrackType: case ID_TrackType:
entry.trackType = (int)readNumber(elem); entry.trackType = (int) readNumber(elem);
break; break;
case ID_CodecID: case ID_CodecID:
entry.codecId = readString(elem); entry.codecId = readString(elem);
@ -445,7 +447,7 @@ public class WebMReader {
public class SimpleBlock { public class SimpleBlock {
public TrackDataChunk data; public InputStream data;
SimpleBlock(Element ref) { SimpleBlock(Element ref) {
this.ref = ref; this.ref = ref;
@ -492,7 +494,7 @@ public class WebMReader {
currentSimpleBlock = readSimpleBlock(elem); currentSimpleBlock = readSimpleBlock(elem);
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize); currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
return currentSimpleBlock; return currentSimpleBlock;
} }

View File

@ -1,20 +1,20 @@
package org.schabi.newpipe.streams; package org.schabi.newpipe.streams;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.Segment;
import org.schabi.newpipe.streams.WebMReader.SimpleBlock; import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack; import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import org.schabi.newpipe.streams.io.SharpStream;
/** /**
*
* @author kapodamy * @author kapodamy
*/ */
public class WebMWriter { public class WebMWriter {
@ -94,10 +94,6 @@ public class WebMWriter {
} }
} }
public long getBytesWritten() {
return written;
}
public boolean isDone() { public boolean isDone() {
return done; return done;
} }
@ -200,7 +196,6 @@ public class WebMWriter {
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32); ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
ArrayList<Long> clusterOffsets = new ArrayList<>(32); ArrayList<Long> clusterOffsets = new ArrayList<>(32);
ArrayList<Integer> clusterSizes = new ArrayList<>(32); ArrayList<Integer> clusterSizes = new ArrayList<>(32);
@ -283,24 +278,21 @@ public class WebMWriter {
long segmentSize = written - offsetSegmentSizeSet - 7; long segmentSize = written - offsetSegmentSizeSet - 7;
// final step write offsets and sizes /* ---- final step write offsets and sizes ---- */
out.rewind(); seekTo(out, offsetSegmentSizeSet);
written = 0;
skipTo(out, offsetSegmentSizeSet);
writeLong(out, segmentSize); writeLong(out, segmentSize);
if (predefinedDurations[durationFromTrackId] > -1) { if (predefinedDurations[durationFromTrackId] > -1) {
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
} }
skipTo(out, offsetInfoDurationSet); seekTo(out, offsetInfoDurationSet);
writeFloat(out, duration); writeFloat(out, duration);
firstClusterOffset -= baseSegmentOffset; firstClusterOffset -= baseSegmentOffset;
skipTo(out, offsetClusterSet); seekTo(out, offsetClusterSet);
writeInt(out, firstClusterOffset); writeInt(out, firstClusterOffset);
skipTo(out, cueReservedOffset); seekTo(out, cueReservedOffset);
/* Cue */ /* Cue */
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
@ -321,17 +313,14 @@ public class WebMWriter {
voidBuffer.putShort((short) (firstClusterOffset - written - 4)); voidBuffer.putShort((short) (firstClusterOffset - written - 4));
dump(voidBuffer.array(), out); dump(voidBuffer.array(), out);
out.rewind(); seekTo(out, offsetCuesSet);
written = 0;
skipTo(out, offsetCuesSet);
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset)); writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
skipTo(out, cueReservedOffset + 5); seekTo(out, cueReservedOffset + 5);
writeShort(out, cueSize); writeShort(out, cueSize);
for (int i = 0; i < clusterSizes.size(); i++) { for (int i = 0; i < clusterSizes.size(); i++) {
skipTo(out, clusterOffsets.get(i)); seekTo(out, clusterOffsets.get(i));
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array(); byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
out.write(size, 1, 3); out.write(size, 1, 3);
written += 3; written += 3;
@ -365,20 +354,29 @@ public class WebMWriter {
bloq.dataSize = (int) res.dataSize; bloq.dataSize = (int) res.dataSize;
bloq.trackNumber = internalTrackId; bloq.trackNumber = internalTrackId;
bloq.flags = res.flags; bloq.flags = res.flags;
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE); bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode; bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
return bloq; return bloq;
} }
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) { private short convertTimecode(int time, long oldTimeScale) {
return (short) (time * (newTimeScale / oldTimeScale)); return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
} }
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException { private void seekTo(SharpStream stream, long offset) throws IOException {
absoluteOffset -= written; if (stream.canSeek()) {
written += absoluteOffset; stream.seek(offset);
stream.skip(absoluteOffset); } else {
if (offset > written) {
stream.skip(offset - written);
} else {
stream.rewind();
stream.skip(offset);
}
}
written = offset;
} }
private void writeLong(SharpStream stream, long number) throws IOException { private void writeLong(SharpStream stream, long number) throws IOException {
@ -618,9 +616,10 @@ public class WebMWriter {
int offset = withLength ? 1 : 0; int offset = withLength ? 1 : 0;
byte[] buffer = new byte[offset + length]; byte[] buffer = new byte[offset + length];
long marker = (long) Math.floor((length - 1) / 8); long marker = (long) Math.floor((length - 1f) / 8f);
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) { float mul = 1;
for (int i = length - 1; i >= 0; i--, mul *= 0x100) {
long b = (long) Math.floor(number / mul); long b = (long) Math.floor(number / mul);
if (!withLength && i == marker) { if (!withLength && i == marker) {
b = b | (0x80 >> (length - 1)); b = b | (0x80 >> (length - 1));
@ -637,11 +636,7 @@ public class WebMWriter {
private ArrayList<byte[]> encode(String value) { private ArrayList<byte[]> encode(String value) {
byte[] str; byte[] str;
try { str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
str = value.getBytes("utf-8");
} catch (UnsupportedEncodingException err) {
str = value.getBytes();
}
ArrayList<byte[]> buffer = new ArrayList<>(2); ArrayList<byte[]> buffer = new ArrayList<>(2);
buffer.add(encode(str.length, false)); buffer.add(encode(str.length, false));
@ -720,9 +715,10 @@ public class WebMWriter {
return (flags & 0x80) == 0x80; return (flags & 0x80) == 0x80;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode); return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode);
} }
} }
} }

View File

@ -3,7 +3,7 @@ package org.schabi.newpipe.streams.io;
import java.io.IOException; import java.io.IOException;
/** /**
* based c# * based on c#
*/ */
public abstract class SharpStream { public abstract class SharpStream {
@ -15,23 +15,27 @@ public abstract class SharpStream {
public abstract long skip(long amount) throws IOException; public abstract long skip(long amount) throws IOException;
public abstract long available();
public abstract int available();
public abstract void rewind() throws IOException; public abstract void rewind() throws IOException;
public abstract void dispose(); public abstract void dispose();
public abstract boolean isDisposed(); public abstract boolean isDisposed();
public abstract boolean canRewind(); public abstract boolean canRewind();
public abstract boolean canRead(); public abstract boolean canRead();
public abstract boolean canWrite(); public abstract boolean canWrite();
public boolean canSetLength() {
return false;
}
public boolean canSeek() {
return false;
}
public abstract void write(byte value) throws IOException; public abstract void write(byte value) throws IOException;
@ -39,9 +43,15 @@ public abstract class SharpStream {
public abstract void write(byte[] buffer, int offset, int count) throws IOException; public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public abstract void flush() throws IOException; public void flush() throws IOException {
// STUB
}
public void setLength(long length) throws IOException { public void setLength(long length) throws IOException {
throw new IOException("Not implemented"); throw new IOException("Not implemented");
} }
public void seek(long offset) throws IOException {
throw new IOException("Not implemented");
}
} }

View File

@ -430,24 +430,26 @@ public final class ListHelper {
*/ */
private static String getResolutionLimit(Context context) { private static String getResolutionLimit(Context context) {
String resolutionLimit = null; String resolutionLimit = null;
if (!isWifiActive(context)) { if (isMeteredNetwork(context)) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String defValue = context.getString(R.string.limit_data_usage_none_key); String defValue = context.getString(R.string.limit_data_usage_none_key);
String value = preferences.getString( String value = preferences.getString(
context.getString(R.string.limit_mobile_data_usage_key), defValue); context.getString(R.string.limit_mobile_data_usage_key), defValue);
resolutionLimit = value.equals(defValue) ? null : value; resolutionLimit = defValue.equals(value) ? null : value;
} }
return resolutionLimit; return resolutionLimit;
} }
/** /**
* Are we connected to wifi? * The current network is metered (like mobile data)?
* @param context App context * @param context App context
* @return {@code true} if connected to wifi * @return {@code true} if connected to a metered network
*/ */
private static boolean isWifiActive(Context context) private static boolean isMeteredNetwork(Context context)
{ {
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; if (manager == null || manager.getActiveNetworkInfo() == null) return false;
return manager.isActiveNetworkMetered();
} }
} }

View File

@ -38,7 +38,7 @@ public class SecondaryStreamHelper<T extends Stream> {
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) { public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
switch (videoStream.getFormat()) { switch (videoStream.getFormat()) {
case WEBM: case WEBM:
case MPEG_4: case MPEG_4:// ¿is mpeg-4 DASH?
break; break;
default: default:
return null; return null;

View File

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

View File

@ -40,6 +40,9 @@ public class DownloadMission extends Mission {
public static final int ERROR_UNKNOWN_HOST = 1005; public static final int ERROR_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 = 1007; 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_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
@ -83,8 +86,9 @@ public class DownloadMission extends Mission {
* 0: ready * 0: ready
* 1: running * 1: running
* 2: completed * 2: completed
* 3: hold
*/ */
public int postprocessingState; public volatile int postprocessingState;
/** /**
* Indicate if the post-processing algorithm works on the same file * Indicate if the post-processing algorithm works on the same file
@ -92,19 +96,19 @@ public class DownloadMission extends Mission {
public boolean postprocessingThis; 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; public int current;
/** /**
* Metadata where the mission state is saved * Metadata where the mission state is saved
*/ */
public File metadata; public transient File metadata;
/** /**
* maximum attempts * maximum attempts
*/ */
public int maxRetry; public transient int maxRetry;
/** /**
* Approximated final length, this represent the sum of all resources sizes * Approximated final length, this represent the sum of all resources sizes
@ -115,11 +119,11 @@ public class DownloadMission extends Mission {
boolean fallback; boolean fallback;
private int finishCount; private int finishCount;
public transient boolean running; public transient boolean running;
public transient boolean enqueued = true; public boolean enqueued;
public int errCode = ERROR_NOTHING; public int errCode = ERROR_NOTHING;
public transient Exception errObject = null; public Exception errObject = null;
public transient boolean recovered; public transient boolean recovered;
public transient Handler mHandler; public transient Handler mHandler;
private transient boolean mWritingToFile; private transient boolean mWritingToFile;
@ -131,7 +135,7 @@ public class DownloadMission extends Mission {
private transient boolean deleted; private transient boolean deleted;
int currentThreadCount; int currentThreadCount;
private transient Thread[] threads = new Thread[0]; public transient volatile Thread[] threads = new Thread[0];
private transient Thread init = null; private transient Thread init = null;
@ -155,6 +159,8 @@ public class DownloadMission extends Mission {
this.location = location; this.location = location;
this.kind = kind; this.kind = kind;
this.offsets = new long[urls.length]; this.offsets = new long[urls.length];
this.enqueued = true;
this.maxRetry = 3;
if (postprocessingName != null) { if (postprocessingName != null) {
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
@ -183,6 +189,7 @@ public class DownloadMission extends Mission {
*/ */
boolean isBlockPreserved(long block) { boolean isBlockPreserved(long block) {
checkBlock(block); checkBlock(block);
//noinspection ConstantConditions
return blockState.containsKey(block) ? blockState.get(block) : false; return blockState.containsKey(block) ? blockState.get(block) : false;
} }
@ -247,6 +254,12 @@ public class DownloadMission extends Mission {
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(true); 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) { if (rangeStart >= 0) {
String req = "bytes=" + rangeStart + "-"; String req = "bytes=" + rangeStart + "-";
if (rangeEnd > 0) req += rangeEnd; 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); 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; errCode = code;
errObject = err; errObject = err;
enqueued = false;
pause(); pause();
@ -378,6 +412,7 @@ public class DownloadMission extends Mission {
if (!doPostprocessing()) return; if (!doPostprocessing()) return;
enqueued = false;
running = false; running = false;
deleteThisFromFile(); deleteThisFromFile();
@ -386,7 +421,6 @@ public class DownloadMission extends Mission {
} }
private void notifyPostProcessing(int state) { private void notifyPostProcessing(int state) {
if (DEBUG) {
String action; String action;
switch (state) { switch (state) {
case 1: case 1:
@ -400,7 +434,6 @@ public class DownloadMission extends Mission {
} }
Log.d(TAG, action + " postprocessing on " + location + File.separator + name); Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
}
synchronized (blockState) { synchronized (blockState) {
// don't return without fully write the current state // don't return without fully write the current state
@ -420,7 +453,6 @@ public class DownloadMission extends Mission {
if (threads != null) if (threads != null)
for (Thread thread : threads) joinForThread(thread); for (Thread thread : threads) joinForThread(thread);
enqueued = false;
running = true; running = true;
errCode = ERROR_NOTHING; 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() { public synchronized void pause() {
if (!running) return; if (!running) return;
@ -477,7 +509,6 @@ public class DownloadMission extends Mission {
running = false; running = false;
recovered = true; recovered = true;
enqueued = false;
if (init != null && Thread.currentThread() != init && init.isAlive()) { if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt(); 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 @Override
public boolean delete() { public boolean delete() {
@ -580,12 +611,21 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false * @return true, otherwise, false
*/ */
public boolean isPsRunning() { 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() { public long getLength() {
long calculated; long calculated;
if (postprocessingState == 1) { if (postprocessingState == 1 || postprocessingState == 3) {
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;
@ -596,14 +636,38 @@ public class DownloadMission extends Mission {
return calculated > nearLength ? calculated : nearLength; 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() { private boolean doPostprocessing() {
if (postprocessingName == null || postprocessingState == 2) return true; if (postprocessingName == null || postprocessingState == 2) return true;
notifyPostProcessing(1); notifyPostProcessing(1);
notifyProgress(0); notifyProgress(0);
if (DEBUG)
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
threads = new Thread[]{Thread.currentThread()};
Exception exception = null; Exception exception = null;
try { try {

View File

@ -47,6 +47,11 @@ public abstract class Mission implements Serializable {
return new File(location, name); 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() { public boolean delete() {
deleted = true; deleted = true;
return getDownloadedFile().delete(); return getDownloadedFile().delete();

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package us.shandian.giga.postprocessing; package us.shandian.giga.postprocessing;
import android.os.Message; import android.os.Message;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream; 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.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream; 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 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 { 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_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_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) { public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) { if (null == algorithmName) {
@ -27,12 +33,12 @@ public abstract class Postprocessing {
} else switch (algorithmName) { } else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER: case ALGORITHM_TTML_CONVERTER:
return new TtmlConverter(mission); 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: case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission); 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": /*case "example-algorithm":
return new ExampleAlgorithm(mission);*/ return new ExampleAlgorithm(mission);*/
default: default:
@ -65,7 +71,8 @@ public abstract class Postprocessing {
public void run() throws IOException { public void run() throws IOException {
File file = mission.getDownloadedFile(); File file = mission.getDownloadedFile();
CircularFile out = null; File temp = null;
CircularFileWriter out = null;
int result; int result;
long finalLength = -1; long finalLength = -1;
@ -81,29 +88,54 @@ public abstract class Postprocessing {
} }
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0}; if (test(sources)) {
CircularFile.OffsetChecker checker = () -> { for (SharpStream source : sources) source.rewind();
while (idx[0] < sources.length) {
OffsetChecker checker = () -> {
for (ChunkFileInputStream source : sources) {
/* /*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks) * WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results * or the CircularFileWriter can lead to unexpected results
*/ */
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { if (source.isDisposed() || source.available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore continue;// the selected source is not used anymore
} }
return sources[idx[0]].getFilePointer() - 1; return source.getFilePointer() - 1;
} }
return -1; return -1;
}; };
out = new CircularFile(file, 0, this::progressReport, checker);
temp = new File(mission.location, mission.name + ".tmp");
out = new CircularFileWriter(file, temp, checker);
out.onProgress = this::progressReport;
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); result = process(out, sources);
if (result == OK_RESULT) if (result == OK_RESULT)
finalLength = out.finalizeFile(); finalLength = out.finalizeFile();
} else {
result = OK_RESULT;
}
} finally { } finally {
for (SharpStream source : sources) { for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) { if (source != null && !source.isDisposed()) {
@ -113,17 +145,22 @@ public abstract class Postprocessing {
if (out != null) { if (out != null) {
out.dispose(); out.dispose();
} }
if (temp != null) {
//noinspection ResultOfMethodCallIgnored
temp.delete();
}
} }
} else { } else {
result = process(null); result = test() ? process(null) : OK_RESULT;
} }
if (result == OK_RESULT) { if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length(); if (finalLength != -1) {
mission.done = finalLength; mission.done = finalLength;
mission.length = finalLength; mission.length = finalLength;
}
} else { } else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION; mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result); 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 out output stream
* @param sources files to be processed * @param sources files to be processed
@ -151,7 +199,7 @@ public abstract class Postprocessing {
return mission.postprocessingArgs[index]; return mission.postprocessingArgs[index];
} }
void progressReport(long done) { private 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

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

View File

@ -1,375 +0,0 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
public class CircularFile extends SharpStream {
private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB
private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false;
private RandomAccessFile out;
private long position;
private long maxLengthKnown = -1;
private ArrayList<ManagedBuffer> auxiliaryBuffers;
private OffsetChecker callback;
private ManagedBuffer queue;
private long startOffset;
private ProgressReport onProgress;
private long reportPosition;
public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException {
if (checker == null) {
throw new NullPointerException("checker is null");
}
try {
queue = new ManagedBuffer(QUEUE_BUFFER_SIZE);
out = new RandomAccessFile(file, "rw");
out.seek(offset);
position = offset;
} catch (IOException err) {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
// nothing to do
}
throw err;
}
auxiliaryBuffers = new ArrayList<>(15);
callback = checker;
startOffset = offset;
reportPosition = offset;
onProgress = progressReport;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void dispose() {
try {
auxiliaryBuffers = null;
if (out != null) {
out.close();
out = null;
}
} catch (IOException err) {
// nothing to do
}
}
/**
* Flush any buffer and close the output file. Use this method if the
* operation is successful
*
* @return the final length of the file
* @throws IOException if an I/O error occurs
*/
public long finalizeFile() throws IOException {
flushEverything();
if (maxLengthKnown > -1) {
position = maxLengthKnown;
}
if (position < out.length()) {
out.setLength(position);
}
dispose();
return position;
}
@Override
public void write(byte b) throws IOException {
write(new byte[]{b}, 0, 1);
}
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
if (len == 0) {
return;
}
long end = callback.check();
long available;
if (end == -1) {
available = Long.MAX_VALUE;
} else {
if (end < startOffset) {
throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end));
}
available = end - position;
}
// Check if possible flush one or more auxiliary buffer
if (auxiliaryBuffers.size() > 0) {
ManagedBuffer aux = auxiliaryBuffers.get(0);
// check if there is enough space to flush it completely
while (available >= (aux.size + queue.size)) {
available -= aux.size;
writeQueue(aux.buffer, 0, aux.size);
aux.dereference();
auxiliaryBuffers.remove(0);
if (auxiliaryBuffers.size() < 1) {
aux = null;
break;
}
aux = auxiliaryBuffers.get(0);
}
if (IMMEDIATE_AUX_BUFFER_FLUSH) {
// try partial flush to avoid allocate another auxiliary buffer
if (aux != null && aux.available() < len && available > queue.size) {
int size = Math.min(aux.size, (int) available - queue.size);
writeQueue(aux.buffer, 0, size);
aux.dereference(size);
available -= size;
}
}
}
if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) {
writeQueue(b, off, len);
} else {
int i = auxiliaryBuffers.size() - 1;
while (len > 0) {
if (i < 0) {
// allocate a new auxiliary buffer
auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE));
i++;
}
ManagedBuffer aux = auxiliaryBuffers.get(i);
available = aux.available();
if (available < 1) {
// secondary auxiliary buffer
available = len;
aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2));
auxiliaryBuffers.add(aux);
i++;
} else {
available = Math.min(len, available);
}
aux.write(b, off, (int) available);
len -= available;
if (len > 0) off += available;
}
}
}
private void writeOutside(byte buffer[], int offset, int length) throws IOException {
out.write(buffer, offset, length);
position += length;
if (onProgress != null && position > reportPosition) {
reportPosition = position + NOTIFY_BYTES_INTERVAL;
onProgress.report(position);
}
}
private void writeQueue(byte[] buffer, int offset, int length) throws IOException {
while (length > 0) {
if (queue.available() < length) {
flushQueue();
if (length >= queue.buffer.length) {
writeOutside(buffer, offset, length);
return;
}
}
int size = Math.min(queue.available(), length);
queue.write(buffer, offset, size);
offset += size;
length -= size;
}
if (queue.size >= queue.buffer.length) {
flushQueue();
}
}
private void flushQueue() throws IOException {
writeOutside(queue.buffer, 0, queue.size);
queue.size = 0;
}
private void flushEverything() throws IOException {
flushQueue();
if (auxiliaryBuffers.size() > 0) {
for (ManagedBuffer aux : auxiliaryBuffers) {
writeOutside(aux.buffer, 0, aux.size);
aux.dereference();
}
auxiliaryBuffers.clear();
}
}
/**
* Flush any buffer directly to the file. Warning: use this method ONLY if
* all read dependencies are disposed
*
* @throws IOException if the dependencies are not disposed
*/
@Override
public void flush() throws IOException {
if (callback.check() != -1) {
throw new IOException("All read dependencies of this file must be disposed first");
}
flushEverything();
// Save the current file length in case the method {@code rewind()} is called
if (position > maxLengthKnown) {
maxLengthKnown = position;
}
}
@Override
public void rewind() throws IOException {
flush();
out.seek(startOffset);
if (onProgress != null) {
onProgress.report(-position);
}
position = startOffset;
reportPosition = startOffset;
}
@Override
public long skip(long amount) throws IOException {
flush();
position += amount;
out.seek(position);
return amount;
}
@Override
public boolean isDisposed() {
return out == null;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
//<editor-fold defaultState="collapsed" desc="stub read methods">
@Override
public boolean canRead() {
return false;
}
@Override
public int read() {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer, int offset, int count) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int available() {
throw new UnsupportedOperationException("write-only");
}
//</editor-fold>
public interface OffsetChecker {
/**
* Checks the amount of available space ahead
*
* @return absolute offset in the file where no more data SHOULD NOT be
* written. If the value is -1 the whole file will be used
*/
long check();
}
public interface ProgressReport {
void report(long progress);
}
class ManagedBuffer {
byte[] buffer;
int size;
ManagedBuffer(int length) {
buffer = new byte[length];
}
void dereference() {
buffer = null;
size = 0;
}
void dereference(int amount) {
if (amount > size) {
throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")");
}
size -= amount;
System.arraycopy(buffer, amount, buffer, 0, size);
}
protected int available() {
return buffer.length - size;
}
private void write(byte[] b, int off, int len) {
System.arraycopy(b, off, buffer, size, len);
size += len;
}
@Override
public String toString() {
return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available());
}
}
}

View File

@ -0,0 +1,459 @@
package us.shandian.giga.postprocessing.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class CircularFileWriter extends SharpStream {
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB
private OffsetChecker callback;
public ProgressReport onProgress;
public WriteErrorHandle onWriteError;
private long reportPosition;
private long maxLengthKnown = -1;
private BufferedFile out;
private BufferedFile aux;
public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException {
if (checker == null) {
throw new NullPointerException("checker is null");
}
if (!temp.exists()) {
if (!temp.createNewFile()) {
throw new IOException("Cannot create a temporal file");
}
}
aux = new BufferedFile(temp);
out = new BufferedFile(source);
callback = checker;
reportPosition = NOTIFY_BYTES_INTERVAL;
}
private void flushAuxiliar() throws IOException {
if (aux.length < 1) {
return;
}
boolean underflow = out.getOffset() >= out.length;
out.flush();
aux.flush();
aux.target.seek(0);
out.target.seek(out.length);
long length = aux.length;
out.length += aux.length;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length));
out.writeProof(aux.queue, read);
length -= read;
}
if (underflow) {
out.offset += aux.offset;
out.target.seek(out.offset);
} else {
out.offset = out.length;
}
if (out.length > maxLengthKnown) {
maxLengthKnown = out.length;
}
if (aux.length > THRESHOLD_AUX_LENGTH) {
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
}
aux.reset();
}
/**
* Flush any buffer and close the output file. Use this method if the
* operation is successful
*
* @return the final length of the file
* @throws IOException if an I/O error occurs
*/
public long finalizeFile() throws IOException {
flushAuxiliar();
out.flush();
// change file length (if required)
long length = Math.max(maxLengthKnown, out.length);
if (length != out.target.length()) {
out.target.setLength(length);
}
dispose();
return length;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void dispose() {
if (out != null) {
out.dispose();
out = null;
}
if (aux != null) {
aux.dispose();
aux = null;
}
}
@Override
public void write(byte b) throws IOException {
write(new byte[]{b}, 0, 1);
}
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
if (len == 0) {
return;
}
long available;
long offsetOut = out.getOffset();
long offsetAux = aux.getOffset();
long end = callback.check();
if (end == -1) {
available = Integer.MAX_VALUE;
} else if (end < offsetOut) {
throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut));
} else {
available = end - offsetOut;
}
boolean usingAux = aux.length > 0 && offsetOut >= out.length;
boolean underflow = offsetAux < aux.length || offsetOut < out.length;
if (usingAux) {
// before continue calculate the final length of aux
long length = offsetAux + len;
if (underflow) {
if (aux.length > length) {
length = aux.length;// the length is not changed
}
} else {
length = aux.length + len;
}
if (length > available || length < THRESHOLD_AUX_LENGTH) {
aux.write(b, off, len);
} else {
if (underflow) {
aux.write(b, off, len);
flushAuxiliar();
} else {
flushAuxiliar();
out.write(b, off, len);// write directly on the output
}
}
} else {
if (underflow) {
available = out.length - offsetOut;
}
int length = Math.min(len, (int) available);
out.write(b, off, length);
len -= length;
off += length;
if (len > 0) {
aux.write(b, off, len);
}
}
if (onProgress != null) {
long absoluteOffset = out.getOffset() + aux.getOffset();
if (absoluteOffset > reportPosition) {
reportPosition = absoluteOffset + NOTIFY_BYTES_INTERVAL;
onProgress.report(absoluteOffset);
}
}
}
@Override
public void flush() throws IOException {
aux.flush();
out.flush();
long total = out.length + aux.length;
if (total > maxLengthKnown) {
maxLengthKnown = total;// save the current file length in case the method {@code rewind()} is called
}
}
@Override
public long skip(long amount) throws IOException {
seek(out.getOffset() + aux.getOffset() + amount);
return amount;
}
@Override
public void rewind() throws IOException {
if (onProgress != null) {
onProgress.report(-out.length - aux.length);// rollback the whole progress
}
seek(0);
reportPosition = NOTIFY_BYTES_INTERVAL;
}
@Override
public void seek(long offset) throws IOException {
long total = out.length + aux.length;
if (offset == total) {
return;// nothing to do
}
// flush everything, avoid any underflow
flush();
if (offset < 0 || offset > total) {
throw new IOException("desired offset is outside of range=0-" + total + " offset=" + offset);
}
if (offset > out.length) {
out.seek(out.length);
aux.seek(offset - out.length);
} else {
out.seek(offset);
aux.seek(0);
}
}
@Override
public boolean isDisposed() {
return out == null;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
@Override
public boolean canSeek() {
return true;
}
// <editor-fold defaultstate="collapsed" desc="stub read methods">
@Override
public boolean canRead() {
return false;
}
@Override
public int read() {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer
) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer, int offset, int count
) {
throw new UnsupportedOperationException("write-only");
}
@Override
public long available() {
throw new UnsupportedOperationException("write-only");
}
//</editor-fold>
public interface OffsetChecker {
/**
* Checks the amount of available space ahead
*
* @return absolute offset in the file where no more data SHOULD NOT be
* written. If the value is -1 the whole file will be used
*/
long check();
}
public interface ProgressReport {
/**
* Report the size of the new file
*
* @param progress the new size
*/
void report(long progress);
}
public interface WriteErrorHandle {
/**
* Attempts to handle a I/O exception
*
* @param err the cause
* @return {@code true} to retry and continue, otherwise, {@code false}
* and throw the exception
*/
boolean handle(Exception err);
}
class BufferedFile {
protected final RandomAccessFile target;
private long offset;
protected long length;
private byte[] queue;
private int queueSize;
BufferedFile(File file) throws FileNotFoundException {
queue = new byte[QUEUE_BUFFER_SIZE];
target = new RandomAccessFile(file, "rw");
}
protected long getOffset() {
return offset + queueSize;// absolute offset in the file
}
protected void dispose() {
try {
queue = null;
target.close();
} catch (IOException e) {
// nothing to do
}
}
protected void write(byte b[], int off, int len) throws IOException {
while (len > 0) {
// if the queue is full, the method available() will flush the queue
int read = Math.min(available(), len);
// enqueue incoming buffer
System.arraycopy(b, off, queue, queueSize, read);
queueSize += read;
len -= read;
off += read;
}
long total = offset + queueSize;
if (total > length) {
length = total;// save length
}
}
protected void flush() throws IOException {
writeProof(queue, queueSize);
offset += queueSize;
queueSize = 0;
}
protected void rewind() throws IOException {
offset = 0;
target.seek(0);
}
protected int available() throws IOException {
if (queueSize >= queue.length) {
flush();
return queue.length;
}
return queue.length - queueSize;
}
protected void reset() throws IOException {
offset = 0;
length = 0;
target.seek(0);
}
protected void seek(long absoluteOffset) throws IOException {
offset = absoluteOffset;
target.seek(absoluteOffset);
}
protected void writeProof(byte[] buffer, int length) throws IOException {
if (onWriteError == null) {
target.write(buffer, 0, length);
return;
}
while (true) {
try {
target.write(buffer, 0, length);
return;
} catch (Exception e) {
if (!onWriteError.handle(e)) {
throw e;// give up
}
}
}
}
@NonNull
@Override
public String toString() {
String absOffset;
String absLength;
try {
absOffset = Long.toString(target.getFilePointer());
} catch (IOException e) {
absOffset = "[" + e.getLocalizedMessage() + "]";
}
try {
absLength = Long.toString(target.length());
} catch (IOException e) {
absLength = "[" + e.getLocalizedMessage() + "]";
}
return String.format(
"offset=%s length=%s queue=%s absOffset=%s absLength=%s",
offset, length, queueSize, absOffset, absLength
);
}
}
}

View File

@ -1,126 +0,0 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* @author kapodamy
*/
public class FileStream extends SharpStream {
public enum Mode {
Read,
ReadWrite
}
public RandomAccessFile source;
private final Mode mode;
public FileStream(String path, Mode mode) throws IOException {
String flags;
if (mode == Mode.Read) {
flags = "r";
} else {
flags = "rw";
}
this.mode = mode;
source = new RandomAccessFile(path, flags);
}
@Override
public int read() throws IOException {
return source.read();
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
return source.read(b, off, len);
}
@Override
public long skip(long pos) throws IOException {
FileChannel fc = source.getChannel();
fc.position(fc.position() + pos);
return pos;
}
@Override
public int available() {
try {
return (int) (source.length() - source.getFilePointer());
} catch (IOException ex) {
return 0;
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
source.getChannel().position(0);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return mode == Mode.Read || mode == Mode.ReadWrite;
}
@Override
public boolean canWrite() {
return mode == Mode.ReadWrite;
}
@Override
public void write(byte value) throws IOException {
source.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
source.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
source.write(buffer, offset, count);
}
@Override
public void flush() {
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
}

View File

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

View File

@ -21,6 +21,8 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission; import us.shandian.giga.get.Mission;
import us.shandian.giga.get.sqlite.DownloadDataSource; 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 us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -28,7 +30,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManager { public class DownloadManager {
private static final String TAG = DownloadManager.class.getSimpleName(); 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_NOTHING = 0;
public final static int SPECIAL_PENDING = 1; public final static int SPECIAL_PENDING = 1;
@ -45,7 +47,9 @@ public class DownloadManager {
private NetworkState mLastNetworkStatus = NetworkState.Unavailable; private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
int mPrefMaxRetry; int mPrefMaxRetry;
boolean mPrefCrossNetwork; boolean mPrefMeteredDownloads;
boolean mPrefQueueLimit;
private boolean mSelfMissionsControl;
/** /**
* Create a new instance * Create a new instance
@ -152,8 +156,8 @@ public class DownloadManager {
} }
mis.postprocessingState = 0; mis.postprocessingState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
mis.errObject = new RuntimeException("stopped unexpectedly"); mis.errObject = null;
} else if (exists && !dl.isFile()) { } else if (exists && !dl.isFile()) {
// probably a folder, this should never happens // probably a folder, this should never happens
if (!sub.delete()) { if (!sub.delete()) {
@ -162,20 +166,21 @@ public class DownloadManager {
continue; continue;
} }
if (!exists) { if (!exists && mis.isInitialized()) {
// downloaded file deleted, reset mission state // downloaded file deleted, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
m.timestamp = mis.timestamp; m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount; m.threadCount = mis.threadCount;
m.source = mis.source; m.source = mis.source;
m.maxRetry = mis.maxRetry;
m.nearLength = mis.nearLength; m.nearLength = mis.nearLength;
m.setEnqueued(mis.enqueued);
mis = m; mis = m;
} }
mis.running = false; mis.running = false;
mis.recovered = exists; mis.recovered = exists;
mis.metadata = sub; mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler; mis.mHandler = mHandler;
mMissionsPending.add(mis); mMissionsPending.add(mis);
@ -205,7 +210,9 @@ public class DownloadManager {
synchronized (this) { synchronized (this) {
// check for existing pending download // check for existing pending download
DownloadMission pendingMission = getPendingMission(location, name); DownloadMission pendingMission = getPendingMission(location, name);
if (pendingMission != null) { if (pendingMission != null) {
if (pendingMission.running) {
// generate unique filename (?) // generate unique filename (?)
try { try {
name = generateUniqueName(location, name); name = generateUniqueName(location, name);
@ -215,7 +222,13 @@ public class DownloadManager {
Log.i(TAG, "Using " + name); Log.i(TAG, "Using " + name);
} }
} else { } else {
// check for existing finished download // dispose the mission
mMissionsPending.remove(pendingMission);
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
pendingMission.delete();
}
} else {
// check for existing finished download and dispose (if exists)
int index = getFinishedMissionIndex(location, name); int index = getFinishedMissionIndex(location, name);
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
} }
@ -242,14 +255,17 @@ public class DownloadManager {
mission.timestamp = System.currentTimeMillis(); mission.timestamp = System.currentTimeMillis();
} }
mSelfMissionsControl = true;
mMissionsPending.add(mission); 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); Utility.writeToFile(mission.metadata, mission);
if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) { boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
if (canDownloadInCurrentNetwork() && start) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start(); mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
} }
} }
} }
@ -257,13 +273,14 @@ public class DownloadManager {
public void resumeMission(DownloadMission mission) { public void resumeMission(DownloadMission mission) {
if (!mission.running) { if (!mission.running) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start(); mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
} }
} }
public void pauseMission(DownloadMission mission) { public void pauseMission(DownloadMission mission) {
if (mission.running) { if (mission.running) {
mission.setEnqueued(false);
mission.pause(); mission.pause();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
} }
@ -335,7 +352,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.isFinished() && !mission.isPsFailed()) if (mission.running && !mission.isPsFailed() && !mission.isFinished())
count++; count++;
} }
} }
@ -343,12 +360,38 @@ public class DownloadManager {
return count; return count;
} }
void pauseAllMissions() { public void pauseAllMissions(boolean force) {
boolean flag = false;
synchronized (this) { 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);
}
/** /**
* Splits the filename into name and extension * Splits the filename into name and extension
@ -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) { synchronized (this) {
if (mMissionsPending.size() < 1) return false; if (mMissionsPending.size() < 1) return false;
int i = getRunningMissionsCount();
if (i > 0) return true;
if (!canDownloadInCurrentNetwork()) return false; if (!canDownloadInCurrentNetwork()) return false;
for (DownloadMission mission : mMissionsPending) { if (mPrefQueueLimit) {
if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) { for (DownloadMission mission : mMissionsPending)
resumeMission(mission); if (!mission.isFinished() && mission.running) return true;
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() { public MissionIterator getIterator() {
mSelfMissionsControl = true;
return new MissionIterator(); return new MissionIterator();
} }
@ -457,31 +504,43 @@ public class DownloadManager {
private boolean canDownloadInCurrentNetwork() { private boolean canDownloadInCurrentNetwork() {
if (mLastNetworkStatus == NetworkState.Unavailable) return false; 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; if (currentStatus == mLastNetworkStatus) return;
mLastNetworkStatus = currentStatus; mLastNetworkStatus = currentStatus;
if (currentStatus == NetworkState.Unavailable) return;
if (currentStatus == NetworkState.Unavailable) { if (!mSelfMissionsControl || updateOnly) {
return; return;// don't touch anything without the user interaction
} else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) {
return;
} }
boolean flag = false; boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
int running = 0;
int paused = 0;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) { if (mission.isFinished() || mission.isPsRunning()) continue;
flag = true;
if (mission.running && isMetered) {
paused++;
mission.pause(); 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() { void updateMaximumAttempts() {
@ -506,21 +565,24 @@ public class DownloadManager {
), Toast.LENGTH_LONG).show(); ), Toast.LENGTH_LONG).show();
} }
void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) { void checkForRunningMission(String location, String name, DMChecker check) {
boolean listed; MissionCheck result = MissionCheck.None;
boolean finished = false;
synchronized (this) { synchronized (this) {
DownloadMission mission = getPendingMission(location, name); DownloadMission pending = getPendingMission(location, name);
if (mission != null) {
listed = true; if (pending == null) {
if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished;
} else { } else {
listed = getFinishedMissionIndex(location, name) >= 0; if (pending.isFinished()) {
finished = listed; 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 { public class MissionIterator extends DiffUtil.Callback {
@ -592,39 +654,6 @@ public class DownloadManager {
return SPECIAL_NOTHING; 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() { public void start() {
current = getSpecialItems(); current = getSpecialItems();
@ -647,6 +676,32 @@ public class DownloadManager {
return hasFinished; 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 @Override
public int getOldListSize() { public int getOldListSize() {

View File

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

View File

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

View File

@ -35,6 +35,8 @@ public class MissionsFragment extends Fragment {
private boolean mLinear; private boolean mLinear;
private MenuItem mSwitch; private MenuItem mSwitch;
private MenuItem mClear = null; private MenuItem mClear = null;
private MenuItem mStart = null;
private MenuItem mPause = null;
private RecyclerView mList; private RecyclerView mList;
private View mEmpty; private View mEmpty;
@ -54,9 +56,11 @@ public class MissionsFragment extends Fragment {
mBinder = (DownloadManagerService.DMBinder) binder; mBinder = (DownloadManagerService.DMBinder) binder;
mBinder.clearDownloadNotifications(); mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty); mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(mBundle, getView()); mAdapter.deleterLoad(mBundle, getView());
setAdapterButtons();
mBundle = null; mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger()); mBinder.addMissionEventListener(mAdapter.getMessenger());
@ -132,7 +136,7 @@ public class MissionsFragment extends Fragment {
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
mContext = activity.getApplicationContext(); mContext = activity;
} }
@ -154,7 +158,11 @@ public class MissionsFragment extends Fragment {
public void onPrepareOptionsMenu(Menu menu) { public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode); mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list); 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); super.onPrepareOptionsMenu(menu);
} }
@ -168,6 +176,14 @@ public class MissionsFragment extends Fragment {
case R.id.clear_list: case R.id.clear_list:
mAdapter.clearFinishedDownloads(); mAdapter.clearFinishedDownloads();
return true; 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: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -193,9 +209,9 @@ public class MissionsFragment extends Fragment {
int icon; int icon;
if (mLinear) 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; 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.setIcon(icon);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); 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 @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

View File

@ -7,6 +7,18 @@
android:title="@string/grid" android:title="@string/grid"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item android:id="@+id/start_downloads"
android:visible="false"
android:icon="?attr/play"
android:title="@string/start_downloads"
app:showAsAction="ifRoom" />
<item android:id="@+id/pause_downloads"
android:visible="false"
android:icon="?attr/pause"
android:title="@string/pause_downloads"
app:showAsAction="ifRoom" />
<item android:id="@+id/clear_list" <item android:id="@+id/clear_list"
android:visible="false" android:visible="false"
android:icon="?attr/ic_delete" android:icon="?attr/ic_delete"

View File

@ -1,4 +1,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/retry"
android:title="@string/retry" />
<item
android:id="@+id/cancel"
android:title="@string/cancel" />
<item <item
android:id="@+id/start" android:id="@+id/start"
android:title="@string/start" /> android:title="@string/start" />
@ -13,8 +22,8 @@
android:checkable="true"/> android:checkable="true"/>
<item <item
android:id="@+id/open" android:id="@+id/menu_item_share"
android:title="@string/view" /> android:title="@string/share" />
<item <item
android:id="@+id/delete" android:id="@+id/delete"

View File

@ -434,7 +434,7 @@
<string name="permission_denied">系统拒绝该行动</string> <string name="permission_denied">系统拒绝该行动</string>
<string name="download_failed">下载失败</string> <string name="download_failed">下载失败</string>
<string name="download_finished">下载完成</string> <string name="download_finished">下载完成</string>
<string name="download_finished_more">%已下载完毕</string> <string name="download_finished_more">%s已下载完毕</string>
<string name="generate_unique_name">生成独特的名字</string> <string name="generate_unique_name">生成独特的名字</string>
<string name="overwrite">覆写</string> <string name="overwrite">覆写</string>
<string name="overwrite_warning">同名的已下载文件已经存在</string> <string name="overwrite_warning">同名的已下载文件已经存在</string>

View File

@ -159,7 +159,7 @@ abrir en modo popup</string>
<string name="contribution_encouragement">Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone!</string> <string name="contribution_encouragement">Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone!</string>
<string name="read_full_license">Leer licencia</string> <string name="read_full_license">Leer licencia</string>
<string name="contribution_title">Contribuir</string> <string name="contribution_title">Contribuir</string>
<string name="subscribe_button_title">Suscribirse</string> <string name="subscribe_button_title">Suscribirse</string>
<string name="subscribed_button_title">Suscrito</string> <string name="subscribed_button_title">Suscrito</string>
<string name="channel_unsubscribed">Canal no suscrito</string> <string name="channel_unsubscribed">Canal no suscrito</string>
<string name="subscription_change_failed">No se pudo cambiar la suscripción</string> <string name="subscription_change_failed">No se pudo cambiar la suscripción</string>
@ -211,8 +211,8 @@ abrir en modo popup</string>
<item quantity="other">Vídeos</item> <item quantity="other">Vídeos</item>
</plurals> </plurals>
<string name="item_deleted">Elemento eliminado</string> <string name="item_deleted">Elemento eliminado</string>
<string name="delete_item_search_history">¿Desea eliminar este elemento del historial de búsqueda?</string> <string name="delete_item_search_history">¿Desea eliminar este elemento del historial de búsqueda?</string>
<string name="main_page_content">Contenido de la página principal</string> <string name="main_page_content">Contenido de la página principal</string>
<string name="blank_page_summary">Página en blanco</string> <string name="blank_page_summary">Página en blanco</string>
<string name="kiosk_page_summary">Página del kiosco</string> <string name="kiosk_page_summary">Página del kiosco</string>
<string name="subscription_page_summary">Página de suscripción</string> <string name="subscription_page_summary">Página de suscripción</string>
@ -224,7 +224,7 @@ abrir en modo popup</string>
<string name="kiosk">Kiosco</string> <string name="kiosk">Kiosco</string>
<string name="trending">Tendencias</string> <string name="trending">Tendencias</string>
<string name="top_50">Top 50</string> <string name="top_50">Top 50</string>
<string name="show_hold_to_append_summary">Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo</string> <string name="show_hold_to_append_summary">Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo</string>
<string name="background_player_append">En cola en el reproductor de fondo</string> <string name="background_player_append">En cola en el reproductor de fondo</string>
<string name="popup_playing_append">En cola en el reproductor popup</string> <string name="popup_playing_append">En cola en el reproductor popup</string>
<string name="play_all">Reproducir todo</string> <string name="play_all">Reproducir todo</string>
@ -242,7 +242,7 @@ abrir en modo popup</string>
<string name="start_here_on_main">Comenzar a reproducir aquí</string> <string name="start_here_on_main">Comenzar a reproducir aquí</string>
<string name="start_here_on_background">Comenzar aquí en segundo plano</string> <string name="start_here_on_background">Comenzar aquí en segundo plano</string>
<string name="start_here_on_popup">Comenzar aquí en popup</string> <string name="start_here_on_popup">Comenzar aquí en popup</string>
<string name="show_hold_to_append_title">Mostrar consejo \"Mantener para poner en la cola\"</string> <string name="show_hold_to_append_title">Mostrar consejo \"Mantener para poner en la cola\"</string>
<string name="new_and_hot">Nuevo y popular</string> <string name="new_and_hot">Nuevo y popular</string>
<string name="hold_to_append">Mantener para poner en la cola</string> <string name="hold_to_append">Mantener para poner en la cola</string>
<string name="donation_title">Donar</string> <string name="donation_title">Donar</string>
@ -270,7 +270,7 @@ abrir en modo popup</string>
<string name="popup_player">Reproductor de popup</string> <string name="popup_player">Reproductor de popup</string>
<string name="preferred_player_fetcher_notification_title">Obteniendo información…</string> <string name="preferred_player_fetcher_notification_title">Obteniendo información…</string>
<string name="preferred_player_fetcher_notification_message">Cargando contenido solicitado</string> <string name="preferred_player_fetcher_notification_message">Cargando contenido solicitado</string>
<string name="import_data_title">Importar base de datos</string> <string name="import_data_title">Importar base de datos</string>
<string name="export_data_title">Exportar base de datos</string> <string name="export_data_title">Exportar base de datos</string>
<string name="import_data_summary">Reemplazará su historial actual y sus suscripciones</string> <string name="import_data_summary">Reemplazará su historial actual y sus suscripciones</string>
<string name="export_data_summary">Exportar historial, suscripciones y listas de reproducción</string> <string name="export_data_summary">Exportar historial, suscripciones y listas de reproducción</string>
@ -325,6 +325,7 @@ abrir en modo popup</string>
<string name="live">DIRECTO</string> <string name="live">DIRECTO</string>
<string name="live_sync">SINCRONIZAR</string> <string name="live_sync">SINCRONIZAR</string>
<string name="file">Archivo</string> <string name="file">Archivo</string>
<string name="missing_file">Archivo movido o eliminado</string>
<string name="invalid_directory">No existe el directorio</string> <string name="invalid_directory">No existe el directorio</string>
<string name="invalid_source">No existe la fuente del archivo/contenido</string> <string name="invalid_source">No existe la fuente del archivo/contenido</string>
<string name="invalid_file">El archivo no existe o insuficientes permisos para leerlo o escribir en él</string> <string name="invalid_file">El archivo no existe o insuficientes permisos para leerlo o escribir en él</string>
@ -419,6 +420,8 @@ abrir en modo popup</string>
<string name="overwrite">Sobrescribir</string> <string name="overwrite">Sobrescribir</string>
<string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string> <string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string>
<string name="download_already_running">Hay una descarga en curso con este nombre</string> <string name="download_already_running">Hay una descarga en curso con este nombre</string>
<string name="download_already_pending">Hay una descarga pendiente con este nombre</string>
<string name="grid">Mostrar como grilla</string> <string name="grid">Mostrar como grilla</string>
<string name="list">Mostrar como lista</string> <string name="list">Mostrar como lista</string>
<string name="clear_finished_download">Limpiar descargas finalizadas</string> <string name="clear_finished_download">Limpiar descargas finalizadas</string>
@ -426,8 +429,14 @@ abrir en modo popup</string>
<string name="stop">Detener</string> <string name="stop">Detener</string>
<string name="max_retry_msg">Intentos máximos</string> <string name="max_retry_msg">Intentos máximos</string>
<string name="max_retry_desc">Cantidad máxima de intentos antes de cancelar la descarga</string> <string name="max_retry_desc">Cantidad máxima de intentos antes de cancelar la descarga</string>
<string name="pause_downloads_on_mobile">Pausar al cambiar a datos moviles</string> <string name="pause_downloads_on_mobile">Interrumpir en redes medidas</string>
<string name="pause_downloads_on_mobile_desc">Las descargas que no se pueden pausar serán reiniciadas</string> <string name="pause_downloads_on_mobile_desc">Útil al cambiar a Datos Móviles, solo algunas descargas no se pueden suspender</string>
<string name="enable_queue_limit">Limitar cola de descarga</string>
<string name="enable_queue_limit_desc">Solo se permitirá una descarga a la vez</string>
<string name="start_downloads">Iniciar descargas</string>
<string name="pause_downloads">Pausar descargas</string>
<!-- message dialog about download error --> <!-- message dialog about download error -->
<string name="show_error">Mostrar error</string> <string name="show_error">Mostrar error</string>
<string name="label_code">Codigo</string> <string name="label_code">Codigo</string>
@ -439,9 +448,12 @@ abrir en modo popup</string>
<string name="error_connect_host">No se puede conectar con el servidor</string> <string name="error_connect_host">No se puede conectar con el servidor</string>
<string name="error_http_no_content">El servidor no devolvio datos</string> <string name="error_http_no_content">El servidor no devolvio datos</string>
<string name="error_http_unsupported_range">El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1</string> <string name="error_http_unsupported_range">El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1</string>
<string name="error_http_requested_range_not_satisfiable">El rango solicitado no se puede satisfacer</string> <string name="error_http_requested_range_not_satisfiable">No se logro obtener el rango solicitado</string>
<string name="error_http_not_found">No encontrado</string> <string name="error_http_not_found">No encontrado</string>
<string name="error_postprocessing_failed">Fallo el post-procesado</string> <string name="error_postprocessing_failed">Fallo el post-procesado</string>
<string name="error_postprocessing_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string>
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
<string name="unsubscribe">Desuscribirse</string> <string name="unsubscribe">Desuscribirse</string>
<string name="tab_new">Nueva pestaña</string> <string name="tab_new">Nueva pestaña</string>
<string name="tab_choose">Elige la pestaña</string> <string name="tab_choose">Elige la pestaña</string>

View File

@ -25,6 +25,7 @@
<attr name="search_add" format="reference"/> <attr name="search_add" format="reference"/>
<attr name="options" format="reference"/> <attr name="options" format="reference"/>
<attr name="play" format="reference"/> <attr name="play" format="reference"/>
<attr name="pause" format="reference"/>
<attr name="bug" format="reference"/> <attr name="bug" format="reference"/>
<attr name="settings" format="reference"/> <attr name="settings" format="reference"/>
<attr name="ic_hot" format="reference"/> <attr name="ic_hot" format="reference"/>

View File

@ -192,6 +192,7 @@
</string-array> </string-array>
<string name="downloads_cross_network" translatable="false">cross_network_downloads</string> <string name="downloads_cross_network" translatable="false">cross_network_downloads</string>
<string name="downloads_queue_limit" translatable="false">downloads_queue_limit</string>
<string name="default_download_threads" translatable="false">default_download_threads</string> <string name="default_download_threads" translatable="false">default_download_threads</string>

View File

@ -196,6 +196,7 @@
<string name="invalid_url_toast">Invalid URL</string> <string name="invalid_url_toast">Invalid URL</string>
<string name="video_streams_empty">No video streams found</string> <string name="video_streams_empty">No video streams found</string>
<string name="audio_streams_empty">No audio streams found</string> <string name="audio_streams_empty">No audio streams found</string>
<string name="missing_file">File moved or deleted</string>
<string name="invalid_directory">No such folder</string> <string name="invalid_directory">No such folder</string>
<string name="invalid_source">No such file/content source</string> <string name="invalid_source">No such file/content source</string>
<string name="invalid_file">The file doesn\'t exist or permission to read or write to it is lacking</string> <string name="invalid_file">The file doesn\'t exist or permission to read or write to it is lacking</string>
@ -513,6 +514,8 @@
<string name="overwrite">Overwrite</string> <string name="overwrite">Overwrite</string>
<string name="overwrite_warning">A downloaded file with this name already exists</string> <string name="overwrite_warning">A downloaded file with this name already exists</string>
<string name="download_already_running">There is a download in progress with this name</string> <string name="download_already_running">There is a download in progress with this name</string>
<string name="download_already_pending">There is a pending download with this name</string>
<!-- message dialog about download error --> <!-- message dialog about download error -->
<string name="show_error">Show error</string> <string name="show_error">Show error</string>
<string name="label_code">Code</string> <string name="label_code">Code</string>
@ -527,13 +530,20 @@
<string name="error_http_requested_range_not_satisfiable">Requested range not satisfiable</string> <string name="error_http_requested_range_not_satisfiable">Requested range not satisfiable</string>
<string name="error_http_not_found">Not found</string> <string name="error_http_not_found">Not found</string>
<string name="error_postprocessing_failed">Post-processing failed</string> <string name="error_postprocessing_failed">Post-processing failed</string>
<string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string>
<string name="error_insufficient_storage">No space left on device</string>
<string name="clear_finished_download">Clear finished downloads</string> <string name="clear_finished_download">Clear finished downloads</string>
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string> <string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
<string name="stop">Stop</string> <string name="stop">Stop</string>
<string name="max_retry_msg">Maximum retries</string> <string name="max_retry_msg">Maximum retries</string>
<string name="max_retry_desc">Maximum number of attempts before canceling the download</string> <string name="max_retry_desc">Maximum number of attempts before canceling the download</string>
<string name="pause_downloads_on_mobile">Pause on switching to mobile data</string> <string name="pause_downloads_on_mobile">Interrupt on metered networks</string>
<string name="pause_downloads_on_mobile_desc">Downloads that can not be paused will be restarted</string> <string name="pause_downloads_on_mobile_desc">Useful when switching to mobile data, although some downloads cannot be suspended</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="enable_queue_limit">Limit download queue</string>
<string name="enable_queue_limit_desc">One download will run at the same time</string>
<string name="start_downloads">Start downloads</string>
<string name="pause_downloads">Pause downloads</string>
</resources> </resources>

View File

@ -41,6 +41,7 @@
<item name="search_add">@drawable/ic_arrow_top_left_black_24dp</item> <item name="search_add">@drawable/ic_arrow_top_left_black_24dp</item>
<item name="options">@drawable/ic_more_vert_black_24dp</item> <item name="options">@drawable/ic_more_vert_black_24dp</item>
<item name="play">@drawable/ic_play_arrow_black_24dp</item> <item name="play">@drawable/ic_play_arrow_black_24dp</item>
<item name="pause">@drawable/ic_pause_black_24dp</item>
<item name="settings">@drawable/ic_settings_black_24dp</item> <item name="settings">@drawable/ic_settings_black_24dp</item>
<item name="ic_hot">@drawable/ic_whatshot_black_24dp</item> <item name="ic_hot">@drawable/ic_whatshot_black_24dp</item>
<item name="ic_channel">@drawable/ic_channel_black_24dp</item> <item name="ic_channel">@drawable/ic_channel_black_24dp</item>
@ -119,6 +120,7 @@
<item name="ic_list">@drawable/ic_list_white_24dp</item> <item name="ic_list">@drawable/ic_list_white_24dp</item>
<item name="ic_grid">@drawable/ic_grid_white_24dp</item> <item name="ic_grid">@drawable/ic_grid_white_24dp</item>
<item name="ic_delete">@drawable/ic_delete_white_24dp</item> <item name="ic_delete">@drawable/ic_delete_white_24dp</item>
<item name="pause">@drawable/ic_pause_white_24dp</item>
<item name="ic_settings_update">@drawable/ic_settings_update_white</item> <item name="ic_settings_update">@drawable/ic_settings_update_white</item>
<item name="separator_color">@color/dark_separator_color</item> <item name="separator_color">@color/dark_separator_color</item>

View File

@ -50,4 +50,11 @@
android:summary="@string/pause_downloads_on_mobile_desc" android:summary="@string/pause_downloads_on_mobile_desc"
android:title="@string/pause_downloads_on_mobile" /> android:title="@string/pause_downloads_on_mobile" />
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/downloads_queue_limit"
android:summary="@string/enable_queue_limit_desc"
android:title="@string/enable_queue_limit" />
</PreferenceScreen> </PreferenceScreen>