1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 16:40:32 +00:00

Merge pull request #2149 from kapodamy/ps-branch

Downloader fixes
This commit is contained in:
Tobias Groza 2019-06-17 22:53:11 +02:00 committed by GitHub
commit 38b0b79644
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 5921 additions and 3427 deletions

View File

@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity {
private void updateFragments() {
MissionsFragment fragment = new MissionsFragment();
getFragmentManager().beginTransaction()
getSupportFragmentManager().beginTransaction()
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();

View File

@ -1,14 +1,23 @@
package org.schabi.newpipe.download;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.DialogFragment;
import android.support.v4.provider.DocumentFile;
import android.support.v7.app.AlertDialog;
import android.support.v7.view.menu.ActionMenuItemView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
@ -34,7 +43,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Localization;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
@ -43,19 +53,27 @@ import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import icepick.Icepick;
import icepick.State;
import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
@State
protected StreamInfo currentInfo;
@ -80,7 +98,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private EditText nameEditText;
private Spinner streamsSpinner;
private RadioGroup radioVideoAudioGroup;
private RadioGroup radioStreamsGroup;
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
@ -160,7 +178,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return;
}
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
@ -177,9 +197,59 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
Intent intent = new Intent(context, DownloadManagerService.class);
context.startService(intent);
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName cname, IBinder service) {
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
mainStorageAudio = mgr.getMainStorageAudio();
mainStorageVideo = mgr.getMainStorageVideo();
downloadManager = mgr.getDownloadManager();
askForSavePath = mgr.askForSavePath();
okButton.setEnabled(true);
context.unbindService(this);
// check of download paths are defined
if (!askForSavePath) {
String msg = "";
if (mainStorageVideo == null) msg += getString(R.string.download_path_title);
if (mainStorageAudio == null)
msg += getString(R.string.download_path_audio_title);
if (!msg.isEmpty()) {
String title;
if (mainStorageVideo == null && mainStorageAudio == null) {
title = getString(R.string.general_error);
msg = getString(R.string.no_available_dir) + ":\n" + msg;
} else {
title = msg;
msg = getString(R.string.no_available_dir);
}
new AlertDialog.Builder(context)
.setPositiveButton(android.R.string.ok, null)
.setTitle(title)
.setMessage(msg)
.create()
.show();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// nothing to do
}
}, Context.BIND_AUTO_CREATE);
}
@Override
@ -204,8 +274,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
threadsCountTextView = view.findViewById(R.id.threads_count);
threadsSeekBar = view.findViewById(R.id.threads);
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
radioVideoAudioGroup.setOnCheckedChangeListener(this);
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
radioStreamsGroup.setOnCheckedChangeListener(this);
initToolbar(view.findViewById(R.id.toolbar));
setupDownloadOptions();
@ -240,17 +310,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
}));
@ -263,22 +333,49 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) {
if (data.getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
}
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
private void initToolbar(Toolbar toolbar) {
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false);// disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
@ -346,7 +443,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG)
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
break;
@ -370,9 +467,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
protected void setupDownloadOptions() {
setRadioButtonsState(false);
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
@ -397,9 +494,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private void setRadioButtonsState(boolean enabled) {
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
}
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
@ -434,92 +531,287 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return 0;
}
StoredDirectoryHelper mainStorageAudio = null;
StoredDirectoryHelper mainStorageVideo = null;
DownloadManager downloadManager = null;
ActionMenuItemView okButton = null;
Context context;
boolean askForSavePath;
private String getNameEditText() {
String str = nameEditText.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
private void showFailedDialog(@StringRes int msg) {
new AlertDialog.Builder(context)
.setTitle(R.string.general_error)
.setMessage(msg)
.setNegativeButton(android.R.string.ok, null)
.create()
.show();
}
private void showErrorActivity(Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
}
private void prepareSelectedDownload() {
final Context context = getContext();
Stream stream;
String location;
char kind;
StoredDirectoryHelper mainStorage;
MediaFormat format;
String mime;
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty())
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
String filename = getNameEditText().concat(".");
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
location = NewPipeSettings.getAudioDownloadPath(context);
kind = 'a';
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
break;
case R.id.video_button:
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
location = NewPipeSettings.getVideoDownloadPath(context);
kind = 'v';
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
break;
case R.id.subtitle_button:
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
kind = 's';
mainStorage = mainStorageVideo;// subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
break;
default:
throw new RuntimeException("No stream selected");
}
if (mainStorage == null || askForSavePath) {
// This part is called if with SAF preferred:
// * older android version running
// * save path not defined (via download settings)
// * the user as checked the "ask where to download" option
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
}
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
StoredFileHelper storage;
try {
if (mainStorage == null) {
// using SAF on older android version
storage = new StoredFileHelper(context, null, targetFile, "");
} else if (targetFile == null) {
// the file does not exist, but it is probably used in a pending download
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
} else {
// the target filename is already use, attempt to use it
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
}
} catch (Exception e) {
showErrorActivity(e);
return;
}
// check if is our file
MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes int msgBtn;
@StringRes int msgBody;
switch (state) {
case Finished:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_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;
case None:
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
continueSelectedDownload(storage);
return;
} else if (targetFile == null) {
// This part is called if:
// * the filename is not used in a pending/finished download
// * the file does not exists, create
if (!mainStorage.mkdirs()) {
showFailedDialog(R.string.error_path_creation);
return;
}
storage = mainStorage.createFile(filename, mime);
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
}
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_unrelated_warning;
break;
default:
return;
}
int threads;
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
threads = 1;// use unique thread for subtitles due small file size
fileName += ".srt";// final subtitle format
} else {
threads = threadsSeekBar.getProgress() + 1;
fileName += "." + stream.getFormat().getSuffix();
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setNegativeButton(android.R.string.cancel, null);
final StoredFileHelper finalStorage = storage;
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
switch (state) {
case Pending:
case Finished:
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
downloadManager.forgetMission(finalStorage);
continueSelectedDownload(finalStorage);
});
break;
}
final String finalFileName = fileName;
askDialog.create().show();
return;
}
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
if (listed) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
.setPositiveButton(
finished ? R.string.overwrite : R.string.generate_unique_name,
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dialog.cancel();
})
.create()
.show();
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
StoredFileHelper storageNew;
switch (state) {
case Finished:
case Pending:
downloadManager.forgetMission(finalStorage);
case None:
if (targetFile == null) {
storageNew = mainStorage.createFile(filename, mime);
} else {
downloadSelected(context, stream, location, finalFileName, kind, threads);
try {
// try take (or steal) the file
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
} catch (IOException e) {
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
storageNew = null;
}
}
if (storageNew != null && storageNew.canWrite())
continueSelectedDownload(storageNew);
else
showFailedDialog(R.string.error_file_creation);
break;
case PendingRunning:
storageNew = mainStorage.createUniqueFile(filename, mime);
if (storageNew == null)
showFailedDialog(R.string.error_file_creation);
else
continueSelectedDownload(storageNew);
break;
}
});
askDialog.create().show();
}
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied);
return;
}
// check if the selected file has to be overwritten, by simply checking its length
try {
if (storage.length() > 0) storage.truncate();
} catch (IOException e) {
Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed);
return;
}
Stream selectedStream;
char kind;
int threads = threadsSeekBar.getProgress() + 1;
String[] urls;
String psName = null;
String[] psArgs = null;
String secondaryStreamUrl = null;
long nearLength = 0;
if (selectedStream instanceof VideoStream) {
// more download logic: select muxer, subtitle converter, etc.
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
threads = 1;// use unique thread for subtitles due small file size
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
else
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
}
}
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
break;
case R.id.subtitle_button:
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
@ -527,6 +819,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
"false",// detect youtube duplicate lines
};
}
break;
default:
return;
}
if (secondaryStreamUrl == null) {
urls = new String[]{selectedStream.getUrl()};
@ -534,8 +830,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
}
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
getDialog().dismiss();
dismiss();
}
}

View File

@ -1195,7 +1195,7 @@ public class VideoDetailFragment
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()

View File

@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.report.UserAction;
import java.util.Collections;
import java.util.HashSet;
@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
* If chosen feed already displayed, then we request another feed from another
* subscription, until the subscription table runs out of new items.
* <p>
* This Observer is self-contained and will dispose itself when complete. However, this
* This Observer is self-contained and will close itself when complete. However, this
* does not obey the fragment lifecycle and may continue running in the background
* until it is complete. This is done due to RxJava2 no longer propagate errors once
* an observer is unsubscribed while the thread process is still running.

View File

@ -158,7 +158,7 @@ public class MediaSourceManager {
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() {
if (DEBUG) Log.d(TAG, "dispose() called.");
if (DEBUG) Log.d(TAG, "close() called.");
debouncedSignal.onComplete();
debouncedLoader.dispose();

View File

@ -17,7 +17,9 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
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;

View File

@ -1,30 +1,77 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.preference.Preference;
import android.util.Log;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
private String DOWNLOAD_PATH_PREFERENCE;
import us.shandian.giga.io.StoredDirectoryHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
private String DOWNLOAD_STORAGE_ASK;
private Preference prefPathVideo;
private Preference prefPathAudio;
private Preference prefStorageAsk;
private Context ctx;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initKeys();
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
DOWNLOAD_STORAGE_ASK = getString(R.string.downloads_storage_ask);
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
prefStorageAsk = findPreference(DOWNLOAD_STORAGE_ASK);
updatePreferencesSummary();
updatePathPickers(!defaultPreferences.getBoolean(DOWNLOAD_STORAGE_ASK, false));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
}
if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show();
updatePreferencesSummary();
}
prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> {
updatePathPickers(!(boolean) value);
return true;
});
}
@Override
@ -32,14 +79,89 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResource(R.xml.download_settings);
}
private void initKeys() {
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
@Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
@Override
public void onDetach() {
super.onDetach();
ctx = null;
prefStorageAsk.setOnPreferenceChangeListener(null);
}
private void updatePreferencesSummary() {
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
}
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
String rawUri = defaultPreferences.getString(prefKey, null);
if (rawUri == null || rawUri.isEmpty()) {
target.setSummary(getString(defaultString));
return;
}
if (rawUri.charAt(0) == File.separatorChar) {
target.setSummary(rawUri);
return;
}
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
target.setSummary(new File(URI.create(rawUri)).getPath());
return;
}
try {
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// nothing to do
}
target.setSummary(rawUri);
}
private boolean isFileUri(String path) {
return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE);
}
private boolean hasInvalidPath(String prefKey) {
String value = defaultPreferences.getString(prefKey, null);
return value == null || value.isEmpty();
}
private void updatePathPickers(boolean enabled) {
prefPathVideo.setEnabled(enabled);
prefPathAudio.setEnabled(enabled);
}
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
private void forgetSAFTree(Context ctx, String oldPath) {
if (IGNORE_RELEASE_ON_OLD_PATH) {
return;
}
if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return;
try {
Uri uri = Uri.parse(oldPath);
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
} catch (Exception err) {
Log.e(TAG, "Error revoking old path permissions on " + oldPath, err);
}
}
private void showMessageDialog(@StringRes int title, @StringRes int message) {
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
msg.setTitle(title);
msg.setMessage(message);
msg.setPositiveButton(android.R.string.ok, null);
msg.show();
}
@Override
@ -48,36 +170,92 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
}
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
String key = preference.getKey();
int request;
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
request = REQUEST_DOWNLOAD_VIDEO_PATH;
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
request = REQUEST_DOWNLOAD_AUDIO_PATH;
} else {
return super.onPreferenceTreeClick(preference);
}
Intent i;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
} else {
i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
}
}
return super.onPreferenceTreeClick(preference);
startActivityForResult(i, request);
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
"resultCode = [" + resultCode + "], data = [" + data + "]"
);
}
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
if (resultCode != Activity.RESULT_OK) return;
defaultPreferences.edit().putString(key, path).apply();
String key;
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
else
return;
Uri uri = data.getData();
if (uri == null) {
showMessageDialog(R.string.general_error, R.string.invalid_directory);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// steps:
// 1. revoke permissions on the old save path
// 2. acquire permissions on the new save path
// 3. save the new path, if step(2) was successful
final Context ctx = getContext();
if (ctx == null) throw new NullPointerException("getContext()");
forgetSAFTree(ctx, defaultPreferences.getString(key, ""));
try {
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
Log.i(TAG, "Acquiring tree success from " + uri.toString());
if (!mainStorage.canWrite())
throw new IOException("No write permissions on " + uri.toString());
} catch (IOException err) {
Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
showMessageDialog(R.string.general_error, R.string.no_available_dir);
return;
}
} else {
File target = Utils.getFileForUri(data.getData());
if (!target.canWrite()) {
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
return;
}
uri = Uri.fromFile(target);
}
defaultPreferences.edit().putString(key, uri.toString()).apply();
updatePreferencesSummary();
}
}
}

View File

@ -70,37 +70,23 @@ public class NewPipeSettings {
getAudioDownloadFolder(context);
}
public static File getVideoDownloadFolder(Context context) {
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
private static void getVideoDownloadFolder(Context context) {
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
}
public static String getVideoDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_key);
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
private static void getAudioDownloadFolder(Context context) {
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static File getAudioDownloadFolder(Context context) {
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static String getAudioDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_audio_key);
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
}
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
final File dir = getDir(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
return dir;
}
@NonNull
@ -108,19 +94,7 @@ public class NewPipeSettings {
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
}
public static void resetDownloadFolders(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
}
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
}
private static String getNewPipeChildFolderPathForDir(File dir) {
return new File(dir, "NewPipe").getAbsolutePath();
return new File(dir, "NewPipe").toURI().toString();
}
}

View File

@ -1,9 +1,10 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.InputStream;
/**
* @author kapodamy
@ -15,89 +16,239 @@ public class DataReader {
public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4;
private long pos;
public final SharpStream stream;
private final boolean rewind;
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
private long position = 0;
private final SharpStream stream;
private InputStream view;
private int viewSize;
public DataReader(SharpStream stream) {
this.rewind = stream.canRewind();
this.stream = stream;
this.pos = 0L;
this.readOffset = this.readBuffer.length;
}
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);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public final int read() throws IOException {
int value = stream.read();
if (value == -1) {
throw new EOFException();
public short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
pos++;
return value;
}
public final long skipBytes(long amount) throws IOException {
amount = stream.skip(amount);
pos += amount;
return amount;
}
public final long readLong() throws IOException {
public long readLong() throws IOException {
primitiveRead(LONG_SIZE);
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];
return high << 32 | low;
}
public final short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final int read(byte[] buffer) throws IOException {
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public final int read(byte[] buffer, int offset, int count) throws IOException {
int res = stream.read(buffer, offset, count);
pos += res;
public int read(byte[] buffer, int offset, int count) throws IOException {
if (readCount < 0) {
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() {
return stream.available() > 0;
position += total;
return total;
}
public boolean available() {
return readCount > 0 || stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
pos = 0;
if ((position - viewSize) > 0) {
viewSize = 0;// drop view
} else {
viewSize += position;
}
position = 0;
readOffset = readBuffer.length;
}
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 {
byte[] buffer = new byte[amount];
int read = stream.read(buffer, 0, amount);
pos += read;
int read = read(buffer, 0, 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++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
for (int i = 0; i < amount; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
}
}
private final byte[] readBuffer = new byte[BUFFER_SIZE];
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;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import org.schabi.newpipe.streams.io.SharpStream;
/**
* @author kapodamy
*/
@ -35,14 +33,29 @@ public class Mp4DashReader {
private static final int ATOM_TREX = 0x74726578;
private static final int ATOM_TKHD = 0x746B6864;
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_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_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>
private final DataReader stream;
private Mp4Track[] tracks = null;
private int[] brands = null;
private Box box;
private Moof moof;
@ -50,9 +63,10 @@ public class Mp4DashReader {
private boolean chunkZero = false;
private int selectedTrack = -1;
private Box backupBox = null;
public enum TrackKind {
Audio, Video, Other
Audio, Video, Subtitles, Other
}
public Mp4DashReader(SharpStream source) {
@ -65,8 +79,15 @@ public class Mp4DashReader {
}
box = readBox(ATOM_FTYP);
if (parse_ftyp() != BRAND_DASH) {
throw new NoSuchElementException("Main Brand is not dash");
brands = parse_ftyp(box);
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;
@ -84,8 +105,6 @@ public class Mp4DashReader {
break;
case ATOM_MFRA:
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) {
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
} else {
switch (moov.trak[i].mdia.hdlr.subType) {
case HANDLER_VIDE:
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;
return tracks[index];
}
@ -126,7 +156,7 @@ public class Mp4DashReader {
* @return list with a basic info
* @throws IOException if the source stream is not seekeable
*/
public int getFragmentsCount() throws IOException {
int getFragmentsCount() throws IOException {
if (selectedTrack < 0) {
throw new IllegalStateException("track no selected");
}
@ -136,7 +166,6 @@ public class Mp4DashReader {
Box tmp;
int count = 0;
long orig_offset = stream.position();
if (box.type == ATOM_MOOF) {
tmp = box;
@ -162,17 +191,36 @@ public class Mp4DashReader {
ensure(tmp);
} while (stream.available() && (tmp = readBox()) != null);
stream.rewind();
stream.skipBytes((int) orig_offset);
rewind();
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() {
return tracks;
}
public Mp4TrackChunk getNextChunk() throws IOException {
public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
Mp4Track track = tracks[selectedTrack];
while (stream.available()) {
@ -208,7 +256,7 @@ public class Mp4DashReader {
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
} 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) {
@ -228,9 +276,12 @@ public class Mp4DashReader {
continue;// find another chunk
}
Mp4TrackChunk chunk = new Mp4TrackChunk();
Mp4DashChunk chunk = new Mp4DashChunk();
chunk.moof = moof;
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
if (!infoOnly) {
chunk.data = stream.getView(moof.traf.trun.chunkSize);
}
moof = null;
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
@ -269,6 +320,10 @@ public class Mp4DashReader {
b.size = stream.readInt();
b.type = stream.readInt();
if (b.size == 1) {
b.size = stream.readLong();
}
return b;
}
@ -280,6 +335,25 @@ public class Mp4DashReader {
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 {
long skip = ref.offset + ref.size - stream.position();
@ -310,6 +384,14 @@ public class Mp4DashReader {
return null;
}
private Box untilAnyBox(Box ref) throws IOException {
if (stream.position() >= (ref.offset + ref.size)) {
return null;
}
return readBox();
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Box readers">
@ -448,11 +530,18 @@ public class Mp4DashReader {
return obj;
}
private int parse_ftyp() throws IOException {
int brand = stream.readInt();
private int[] parse_ftyp(Box ref) throws IOException {
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
return brand;
for (; i < list.length; i++)
list[i] = stream.readInt();// compatible brands
return list;
}
private Mvhd parse_mvhd() throws IOException {
@ -521,32 +610,66 @@ public class Mp4DashReader {
trak.tkhd = parse_tkhd();
ensure(b);
b = untilBox(ref, ATOM_MDIA);
trak.mdia = new byte[b.size];
while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
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);
buffer.putInt(b.size);
buffer.putInt(ATOM_MDIA);
stream.read(trak.mdia, 8, b.size - 8);
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
ensure(b);
}
return trak;
}
private int parse_mdia(ByteBuffer data) {
while (data.hasRemaining()) {
int end = data.position() + data.getInt();
if (data.getInt() == ATOM_MDHD) {
byte version = data.get();
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
return data.getInt();
private Mdia parse_mdia(Box ref) throws IOException {
Mdia obj = new Mdia();
Box b;
while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
switch (b.type) {
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 {
@ -570,7 +693,7 @@ public class Mp4DashReader {
ensure(b);
}
moov.trak = tmp.toArray(new Trak[tmp.size()]);
moov.trak = tmp.toArray(new Trak[0]);
return moov;
}
@ -584,7 +707,7 @@ public class Mp4DashReader {
ensure(b);
}
return tmp.toArray(new Trex[tmp.size()]);
return tmp.toArray(new Trex[0]);
}
private Trex parse_trex() throws IOException {
@ -602,74 +725,74 @@ public class Mp4DashReader {
return obj;
}
private Tfra parse_tfra() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
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));
private Elst parse_edts(Box ref) throws IOException {
Box b = untilBox(ref, ATOM_ELST);
if (b == null) {
return null;
}
return tfra;
}
private Sidx parse_sidx() throws IOException {
int version = stream.read();
Elst obj = new Elst();
boolean v1 = stream.read() == 1;
stream.skipBytes(3);// flags
Sidx obj = new Sidx();
obj.referenceId = stream.readInt();
obj.timescale = stream.readInt();
int entryCount = stream.readInt();
if (entryCount < 1) {
obj.bMediaRate = 0x00010000;// default media rate (1.0)
return obj;
}
// earliest presentation entries_time
// first offset
// reserved
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
if (v1) {
stream.skipBytes(DataReader.LONG_SIZE);// segment duration
obj.MediaTime = stream.readLong();
// 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++) {
// reference type
// referenced size
stream.skipBytes(4);
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
return obj;
}
// starts with SAP
// SAP type
// SAP delta entries_time
stream.skipBytes(4);
private Minf parse_minf(Box ref) throws IOException {
Minf obj = new Minf();
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;
}
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
long limit = ref.offset + ref.size;
/**
* this only read the "stsd" box inside
*/
private byte[] parse_stbl(Box ref) throws IOException {
Box b = untilBox(ref, ATOM_STSD);
while (stream.position() < limit) {
box = readBox();
if (box.type == ATOM_TFRA) {
tmp.add(parse_tfra());
if (b == null) {
return new byte[0];// this never should happens (missing codec startup data)
}
ensure(box);
}
return tmp.toArray(new Tfra[tmp.size()]);
return readFullBox(b);
}
// </editor-fold>
@ -679,14 +802,7 @@ public class Mp4DashReader {
int type;
long offset;
int size;
}
class Sidx {
int timescale;
int referenceId;
int[] entries_subsegmentDuration;
long size;
}
public class Moof {
@ -711,12 +827,16 @@ public class Mp4DashReader {
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 {
@ -749,6 +869,31 @@ public class Mp4DashReader {
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;
}
}
@ -768,9 +913,9 @@ public class Mp4DashReader {
public class Trak {
public Tkhd tkhd;
public int mdia_mdhd_timeScale;
public Elst edst_elst;
public Mdia mdia;
byte[] mdia;
}
class Mvhd {
@ -786,12 +931,6 @@ public class Mp4DashReader {
Trex[] mvex_trex;
}
class Tfra {
int trackId;
int[] entries_time;
}
public class Trex {
private int trackId;
@ -801,6 +940,34 @@ public class Mp4DashReader {
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 TrackKind kind;
@ -808,10 +975,43 @@ public class Mp4DashReader {
public Trex trex;
}
public class Mp4TrackChunk {
public class Mp4DashChunk {
public InputStream data;
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>
}

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.close();
}
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 {*/
if (auxSize > 0) {
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;
import org.schabi.newpipe.streams.io.SharpStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@ -12,8 +13,6 @@ import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Locale;
import org.schabi.newpipe.streams.io.SharpStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@ -70,7 +69,7 @@ public class SubtitleConverter {
* Language parsing is not supported
*/
byte[] buffer = new byte[source.available()];
byte[] buffer = new byte[(int) source.available()];
source.read(buffer);
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();
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;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
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 {
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 {
@ -193,6 +194,7 @@ public class WebMReader {
return elem;
}
}
ensure(elem);
}
@ -306,7 +308,7 @@ public class WebMReader {
entry.trackNumber = readNumber(elem);
break;
case ID_TrackType:
entry.trackType = (int)readNumber(elem);
entry.trackType = (int) readNumber(elem);
break;
case ID_CodecID:
entry.codecId = readString(elem);
@ -445,7 +447,7 @@ public class WebMReader {
public class SimpleBlock {
public TrackDataChunk data;
public InputStream data;
SimpleBlock(Element ref) {
this.ref = ref;
@ -492,7 +494,7 @@ public class WebMReader {
currentSimpleBlock = readSimpleBlock(elem);
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
return currentSimpleBlock;
}

View File

@ -1,20 +1,20 @@
package org.schabi.newpipe.streams;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment;
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import org.schabi.newpipe.streams.io.SharpStream;
/**
*
* @author kapodamy
*/
public class WebMWriter {
@ -94,10 +94,6 @@ public class WebMWriter {
}
}
public long getBytesWritten() {
return written;
}
public boolean isDone() {
return done;
}
@ -111,7 +107,7 @@ public class WebMWriter {
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
src.close();
}
sourceTracks = null;
@ -200,7 +196,6 @@ public class WebMWriter {
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
@ -283,24 +278,21 @@ public class WebMWriter {
long segmentSize = written - offsetSegmentSizeSet - 7;
// final step write offsets and sizes
out.rewind();
written = 0;
skipTo(out, offsetSegmentSizeSet);
/* ---- final step write offsets and sizes ---- */
seekTo(out, offsetSegmentSizeSet);
writeLong(out, segmentSize);
if (predefinedDurations[durationFromTrackId] > -1) {
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
}
skipTo(out, offsetInfoDurationSet);
seekTo(out, offsetInfoDurationSet);
writeFloat(out, duration);
firstClusterOffset -= baseSegmentOffset;
skipTo(out, offsetClusterSet);
seekTo(out, offsetClusterSet);
writeInt(out, firstClusterOffset);
skipTo(out, cueReservedOffset);
seekTo(out, cueReservedOffset);
/* Cue */
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
@ -321,20 +313,16 @@ public class WebMWriter {
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
dump(voidBuffer.array(), out);
out.rewind();
written = 0;
skipTo(out, offsetCuesSet);
seekTo(out, offsetCuesSet);
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
skipTo(out, cueReservedOffset + 5);
seekTo(out, cueReservedOffset + 5);
writeShort(out, cueSize);
for (int i = 0; i < clusterSizes.size(); i++) {
skipTo(out, clusterOffsets.get(i));
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
out.write(size, 1, 3);
written += 3;
seekTo(out, clusterOffsets.get(i));
byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array();
dump(buffer, out);
}
}
@ -365,20 +353,29 @@ public class WebMWriter {
bloq.dataSize = (int) res.dataSize;
bloq.trackNumber = internalTrackId;
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;
return bloq;
}
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
return (short) (time * (newTimeScale / oldTimeScale));
private short convertTimecode(int time, long oldTimeScale) {
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
}
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
absoluteOffset -= written;
written += absoluteOffset;
stream.skip(absoluteOffset);
private void seekTo(SharpStream stream, long offset) throws IOException {
if (stream.canSeek()) {
stream.seek(offset);
} 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 {
@ -453,7 +450,7 @@ public class WebMWriter {
/* cluster */
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
dump(new byte[]{0x20, 0x00, 0x00}, stream);
dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream);
startOffset = written;// size for the this cluster
@ -618,9 +615,10 @@ public class WebMWriter {
int offset = withLength ? 1 : 0;
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);
if (!withLength && i == marker) {
b = b | (0x80 >> (length - 1));
@ -637,11 +635,7 @@ public class WebMWriter {
private ArrayList<byte[]> encode(String value) {
byte[] str;
try {
str = value.getBytes("utf-8");
} catch (UnsupportedEncodingException err) {
str = value.getBytes();
}
str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
ArrayList<byte[]> buffer = new ArrayList<>(2);
buffer.add(encode(str.length, false));
@ -720,9 +714,10 @@ public class WebMWriter {
return (flags & 0x80) == 0x80;
}
@NonNull
@Override
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

@ -1,11 +1,12 @@
package org.schabi.newpipe.streams.io;
import java.io.Closeable;
import java.io.IOException;
/**
* based c#
* based on c#
*/
public abstract class SharpStream {
public abstract class SharpStream implements Closeable {
public abstract int read() throws IOException;
@ -15,16 +16,14 @@ public abstract class SharpStream {
public abstract long skip(long amount) throws IOException;
public abstract int available();
public abstract long available();
public abstract void rewind() throws IOException;
public abstract boolean isClosed();
public abstract void dispose();
public abstract boolean isDisposed();
@Override
public abstract void close();
public abstract boolean canRewind();
@ -32,6 +31,13 @@ public abstract class SharpStream {
public abstract boolean canWrite();
public boolean canSetLength() {
return false;
}
public boolean canSeek() {
return false;
}
public abstract void write(byte value) throws IOException;
@ -39,9 +45,19 @@ public abstract class SharpStream {
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 {
throw new IOException("Not implemented");
}
public void seek(long offset) throws IOException {
throw new IOException("Not implemented");
}
public long length() throws IOException {
throw new UnsupportedOperationException("Unsupported operation");
}
}

View File

@ -10,6 +10,9 @@ import java.util.regex.Pattern;
public class FilenameUtils {
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
/**
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
* @param context the context to retrieve strings and preferences from
@ -18,11 +21,28 @@ public class FilenameUtils {
*/
public static String createFilename(Context context, String title) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.settings_file_charset_key);
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value));
Pattern pattern = Pattern.compile(value);
final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
final String charset_ms = context.getString(R.string.charset_most_special_value);
final String defaultCharset = context.getString(R.string.default_file_charset_value);
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
final String charset;
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
if (selectedCharset.equals(charset_ld)) {
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
} else if (selectedCharset.equals(charset_ms)) {
charset = CHARSET_MOST_SPECIAL;
} else {
charset = selectedCharset;// ¿is the user using a custom charset?
}
Pattern pattern = Pattern.compile(charset);
return createFilename(title, pattern, replacementChar);
}

View File

@ -430,24 +430,26 @@ public final class ListHelper {
*/
private static String getResolutionLimit(Context context) {
String resolutionLimit = null;
if (!isWifiActive(context)) {
if (isMeteredNetwork(context)) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String defValue = context.getString(R.string.limit_data_usage_none_key);
String value = preferences.getString(
context.getString(R.string.limit_mobile_data_usage_key), defValue);
resolutionLimit = value.equals(defValue) ? null : value;
resolutionLimit = defValue.equals(value) ? null : value;
}
return resolutionLimit;
}
/**
* Are we connected to wifi?
* The current network is metered (like mobile data)?
* @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);
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) {
switch (videoStream.getFormat()) {
case WEBM:
case MPEG_4:
case MPEG_4:// ¿is mpeg-4 DASH?
break;
default:
return null;

View File

@ -3,10 +3,10 @@ package us.shandian.giga.get;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
@ -17,6 +17,8 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadInitializer extends Thread {
private final static String TAG = "DownloadInitializer";
final static int mId = 0;
private final static int RESERVE_SPACE_DEFAULT = 5 * 1024 * 1024;// 5 MiB
private final static int RESERVE_SPACE_MAXIMUM = 150 * 1024 * 1024;// 150 MiB
private DownloadMission mMission;
private HttpURLConnection mConn;
@ -28,22 +30,53 @@ public class DownloadInitializer extends Thread {
@Override
public void run() {
if (mMission.current > 0) mMission.resetState();
if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING);
int retryCount = 0;
while (true) {
try {
mMission.currentThreadCount = mMission.threadCount;
if (mMission.blocks < 0 && mMission.current == 0) {
// calculate the whole size of the mission
long finalLength = 0;
long lowestSize = Long.MAX_VALUE;
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
mMission.establishConnection(mId, mConn);
if (Thread.interrupted()) return;
long length = Utility.getContentLength(mConn);
if (i == 0) mMission.length = length;
if (length > 0) finalLength += length;
if (length < lowestSize) lowestSize = length;
}
mMission.nearLength = finalLength;
// reserve space at the start of the file
if (mMission.psAlgorithm != null && mMission.psAlgorithm.reserveSpace) {
if (lowestSize < 1) {
// the length is unknown use the default size
mMission.offsets[0] = RESERVE_SPACE_DEFAULT;
} else {
// use the smallest resource size to download, otherwise, use the maximum
mMission.offsets[0] = lowestSize < RESERVE_SPACE_MAXIMUM ? lowestSize : RESERVE_SPACE_MAXIMUM;
}
}
} else {
// ask for the current resource length
mConn = mMission.openConnection(mId, -1, -1);
mMission.establishConnection(mId, mConn);
if (!mMission.running || Thread.interrupted()) return;
mMission.length = Utility.getContentLength(mConn);
}
if (mMission.length == 0) {
if (mMission.length == 0 || mConn.getResponseCode() == 204) {
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
return;
}
@ -111,34 +144,10 @@ public class DownloadInitializer extends Thread {
if (!mMission.running || Thread.interrupted()) return;
}
File file;
if (mMission.current == 0) {
file = new File(mMission.location);
if (!Utility.mkdir(file, true)) {
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
return;
}
file = new File(file, mMission.name);
// if the name is used by another process, delete it
if (file.exists() && !file.isFile() && !file.delete()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
if (!file.exists() && !file.createNewFile()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
} else {
file = new File(mMission.location, mMission.name);
}
RandomAccessFile af = new RandomAccessFile(file, "rw");
af.setLength(mMission.offsets[mMission.current] + mMission.length);
af.seek(mMission.offsets[mMission.current]);
af.close();
SharpStream fs = mMission.storage.getStream();
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
fs.seek(mMission.offsets[mMission.current]);
fs.close();
if (!mMission.running || Thread.interrupted()) return;
@ -164,9 +173,6 @@ public class DownloadInitializer extends Thread {
}
}
// hide marquee in the progress bar
mMission.done++;
mMission.start();
}

View File

@ -4,11 +4,14 @@ import android.os.Handler;
import android.os.Message;
import android.util.Log;
import org.schabi.newpipe.Downloader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
@ -17,6 +20,7 @@ import java.util.List;
import javax.net.ssl.SSLException;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.util.Utility;
@ -24,7 +28,7 @@ import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission extends Mission {
private static final long serialVersionUID = 3L;// last bump: 8 november 2018
private static final long serialVersionUID = 4L;// last bump: 27 march 2019
static final int BUFFER_SIZE = 64 * 1024;
final static int BLOCK_SIZE = 512 * 1024;
@ -40,6 +44,11 @@ public class DownloadMission extends Mission {
public static final int ERROR_UNKNOWN_HOST = 1005;
public static final int ERROR_CONNECT_HOST = 1006;
public static final int ERROR_POSTPROCESSING = 1007;
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
public static final int ERROR_PROGRESS_LOST = 1011;
public static final int ERROR_TIMEOUT = 1012;
public static final int ERROR_HTTP_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
@ -68,43 +77,34 @@ public class DownloadMission extends Mission {
*/
public long[] offsets;
/**
* The post-processing algorithm arguments
*/
public String[] postprocessingArgs;
/**
* The post-processing algorithm name
*/
public String postprocessingName;
/**
* Indicates if the post-processing state:
* 0: ready
* 1: running
* 2: completed
* 3: hold
*/
public int postprocessingState;
public volatile int psState;
/**
* Indicate if the post-processing algorithm works on the same file
* the post-processing algorithm instance
*/
public boolean postprocessingThis;
public Postprocessing psAlgorithm;
/**
* The current resource to download {@code urls[current]}
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
*/
public int current;
/**
* Metadata where the mission state is saved
*/
public File metadata;
public transient File metadata;
/**
* maximum attempts
*/
public int maxRetry;
public transient int maxRetry;
/**
* Approximated final length, this represent the sum of all resources sizes
@ -115,11 +115,11 @@ public class DownloadMission extends Mission {
boolean fallback;
private int finishCount;
public transient boolean running;
public transient boolean enqueued = true;
public boolean enqueued;
public int errCode = ERROR_NOTHING;
public transient Exception errObject = null;
public Exception errObject = null;
public transient boolean recovered;
public transient Handler mHandler;
private transient boolean mWritingToFile;
@ -131,43 +131,28 @@ public class DownloadMission extends Mission {
private transient boolean deleted;
int currentThreadCount;
private transient Thread[] threads = new Thread[0];
public transient volatile Thread[] threads = new Thread[0];
private transient Thread init = null;
protected DownloadMission() {
}
public DownloadMission(String url, String name, String location, char kind) {
this(new String[]{url}, name, location, kind, null, null);
}
public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) {
if (name == null) throw new NullPointerException("name is null");
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
if (urls == null) throw new NullPointerException("urls is null");
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
if (location == null) throw new NullPointerException("location is null");
if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
this.urls = urls;
this.name = name;
this.location = location;
this.kind = kind;
this.offsets = new long[urls.length];
this.enqueued = true;
this.maxRetry = 3;
this.storage = storage;
this.psAlgorithm = psInstance;
if (postprocessingName != null) {
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
this.postprocessingThis = algorithm.worksOnSameFile;
this.offsets[0] = algorithm.recommendedReserve;
this.postprocessingName = postprocessingName;
this.postprocessingArgs = postprocessingArgs;
} else {
if (DEBUG && urls.length > 1) {
if (DEBUG && psInstance == null && urls.length > 1) {
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
}
}
}
private void checkBlock(long block) {
if (block < 0 || block >= blocks) {
@ -183,6 +168,7 @@ public class DownloadMission extends Mission {
*/
boolean isBlockPreserved(long block) {
checkBlock(block);
//noinspection ConstantConditions
return blockState.containsKey(block) ? blockState.get(block) : false;
}
@ -243,9 +229,18 @@ public class DownloadMission extends Mission {
* @throws IOException if an I/O exception occurs.
*/
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
URL url = new URL(urls[current]);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
}
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
conn.setRequestProperty("Accept", "*/*");
// BUG workaround: switching between networks can freeze the download forever
conn.setConnectTimeout(30000);
conn.setReadTimeout(10000);
if (rangeStart >= 0) {
String req = "bytes=" + rangeStart + "-";
@ -337,17 +332,42 @@ public class DownloadMission extends Mission {
notifyError(ERROR_CONNECT_HOST, null);
} else if (err instanceof UnknownHostException) {
notifyError(ERROR_UNKNOWN_HOST, null);
} else if (err instanceof SocketTimeoutException) {
notifyError(ERROR_TIMEOUT, null);
} else {
notifyError(ERROR_UNKNOWN_EXCEPTION, err);
}
}
synchronized void notifyError(int code, Exception err) {
public synchronized void notifyError(int code, Exception err) {
Log.e(TAG, "notifyError() code = " + code, err);
if (err instanceof IOException) {
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
code = ERROR_PERMISSION_DENIED;
err = null;
} else if (err.getMessage().contains("ENOSPC")) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
}
}
errCode = code;
errObject = err;
switch (code) {
case ERROR_SSL_EXCEPTION:
case ERROR_UNKNOWN_HOST:
case ERROR_CONNECT_HOST:
case ERROR_TIMEOUT:
// do not change the queue flag for network errors, can be
// recovered silently without the user interaction
break;
default:
// also checks for server errors
if (code < 500 || code > 599) enqueued = false;
}
pause();
notify(DownloadManagerService.MESSAGE_ERROR);
@ -378,6 +398,7 @@ public class DownloadMission extends Mission {
if (!doPostprocessing()) return;
enqueued = false;
running = false;
deleteThisFromFile();
@ -386,7 +407,6 @@ public class DownloadMission extends Mission {
}
private void notifyPostProcessing(int state) {
if (DEBUG) {
String action;
switch (state) {
case 1:
@ -399,12 +419,11 @@ public class DownloadMission extends Mission {
action = "Failed";
}
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
}
Log.d(TAG, action + " postprocessing on " + storage.getName());
synchronized (blockState) {
// don't return without fully write the current state
postprocessingState = state;
psState = state;
Utility.writeToFile(metadata, DownloadMission.this);
}
}
@ -420,11 +439,10 @@ public class DownloadMission extends Mission {
if (threads != null)
for (Thread thread : threads) joinForThread(thread);
enqueued = false;
running = true;
errCode = ERROR_NOTHING;
if (current >= urls.length && postprocessingName != null) {
if (current >= urls.length && psAlgorithm != null) {
runAsync(1, () -> {
if (doPostprocessing()) {
running = false;
@ -463,7 +481,7 @@ public class DownloadMission extends Mission {
}
/**
* Pause the mission, does not affect the blocks that are being downloaded.
* Pause the mission
*/
public synchronized void pause() {
if (!running) return;
@ -477,12 +495,11 @@ public class DownloadMission extends Mission {
running = false;
recovered = true;
enqueued = false;
if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt();
synchronized (blockState) {
resetState();
resetState(false, true, ERROR_NOTHING);
}
return;
}
@ -514,20 +531,31 @@ public class DownloadMission extends Mission {
}
/**
* Removes the file and the meta file
* Removes the downloaded file and the meta file
*/
@Override
public boolean delete() {
deleted = true;
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
boolean res = deleteThisFromFile();
if (!super.delete()) res = false;
if (!super.delete()) return false;
return res;
}
void resetState() {
/**
* Resets the mission state
*
* @param rollback {@code true} true to forget all progress, otherwise, {@code false}
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
*/
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
done = 0;
blocks = -1;
errCode = ERROR_NOTHING;
errCode = errorCode;
errObject = null;
fallback = false;
unknownLength = false;
finishCount = 0;
@ -536,6 +564,9 @@ public class DownloadMission extends Mission {
blockState.clear();
threads = new Thread[0];
if (rollback) current = 0;
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
}
@ -562,7 +593,7 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false
*/
public boolean isFinished() {
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
return current >= urls.length && (psAlgorithm == null || psState == 2);
}
/**
@ -571,7 +602,13 @@ public class DownloadMission extends Mission {
* @return {@code true} if this mission is unrecoverable
*/
public boolean isPsFailed() {
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
switch (errCode) {
case ERROR_POSTPROCESSING:
case ERROR_POSTPROCESSING_STOPPED:
return psAlgorithm.worksOnSameFile;
}
return false;
}
/**
@ -580,12 +617,26 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false
*/
public boolean isPsRunning() {
return postprocessingName != null && postprocessingState == 1;
return psAlgorithm != null && (psState == 1 || psState == 3);
}
/**
* Indicated if the mission is ready
*
* @return true, otherwise, false
*/
public boolean isInitialized() {
return blocks >= 0; // DownloadMissionInitializer was executed
}
/**
* Gets the approximated final length of the file
*
* @return the length in bytes
*/
public long getLength() {
long calculated;
if (postprocessingState == 1) {
if (psState == 1 || psState == 3) {
calculated = length;
} else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
@ -596,30 +647,67 @@ public class DownloadMission extends Mission {
return calculated > nearLength ? calculated : nearLength;
}
/**
* set this mission state on the queue
*
* @param queue true to add to the queue, otherwise, false
*/
public void setEnqueued(boolean queue) {
enqueued = queue;
runAsync(-2, this::writeThisToFile);
}
/**
* Attempts to continue a blocked post-processing
*
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
*/
public void psContinue(boolean recover) {
psState = 1;
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
threads[0].interrupt();
}
/**
* Indicates whatever the backed storage is invalid
*
* @return {@code true}, if storage is invalid and cannot be used
*/
public boolean hasInvalidStorage() {
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile();
}
/**
* Indicates whatever is possible to start the mission
*
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
*/
public boolean isCorrupt() {
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage();
}
private boolean doPostprocessing() {
if (postprocessingName == null || postprocessingState == 2) return true;
if (psAlgorithm == null || psState == 2) return true;
errObject = null;
notifyPostProcessing(1);
notifyProgress(0);
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
if (DEBUG)
Thread.currentThread().setName("[" + TAG + "] ps = " +
psAlgorithm.getClass().getSimpleName() +
" filename = " + storage.getName()
);
threads = new Thread[]{Thread.currentThread()};
Exception exception = null;
try {
Postprocessing
.getAlgorithm(postprocessingName, this)
.run();
psAlgorithm.run(this);
} catch (Exception err) {
StringBuilder args = new StringBuilder(" ");
if (postprocessingArgs != null) {
for (String arg : postprocessingArgs) {
args.append(", ");
args.append(arg);
}
args.delete(0, 1);
}
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
@ -669,7 +757,7 @@ public class DownloadMission extends Mission {
// >=1: any download thread
if (DEBUG) {
who.setName(String.format("%s[%s] %s", TAG, id, name));
who.setName(String.format("%s[%s] %s", TAG, id, storage.getName()));
}
who.start();

View File

@ -2,9 +2,10 @@ package us.shandian.giga.get;
import android.util.Log;
import java.io.FileNotFoundException;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
@ -26,7 +27,6 @@ public class DownloadRunnable extends Thread {
if (mission == null) throw new NullPointerException("mission is null");
mMission = mission;
mId = id;
mConn = null;
}
@Override
@ -40,12 +40,12 @@ public class DownloadRunnable extends Thread {
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
}
RandomAccessFile f;
SharpStream f;
InputStream is = null;
try {
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
} catch (FileNotFoundException e) {
f = mMission.storage.getStream();
} catch (IOException e) {
mMission.notifyError(e);// this never should happen
return;
}
@ -136,6 +136,10 @@ public class DownloadRunnable extends Thread {
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
} catch (Exception e) {
if (DEBUG) {
Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e);
}
mMission.setThreadBytePosition(mId, total);
if (!mMission.running || e instanceof ClosedByInterruptException) break;
@ -145,10 +149,6 @@ public class DownloadRunnable extends Thread {
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
}
retry = true;
}
}

View File

@ -1,16 +1,15 @@
package us.shandian.giga.get;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -19,21 +18,17 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
* Single-threaded fallback mode
*/
public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallback";
private static final String TAG = "DownloadRunnableFallbac";
private final DownloadMission mMission;
private final int mId = 1;
private int mRetryCount = 0;
private InputStream mIs;
private RandomAccessFile mF;
private SharpStream mF;
private HttpURLConnection mConn;
DownloadRunnableFallback(@NonNull DownloadMission mission) {
mMission = mission;
mIs = null;
mF = null;
mConn = null;
}
private void dispose() {
@ -43,15 +38,10 @@ public class DownloadRunnableFallback extends Thread {
// nothing to do
}
try {
if (mF != null) mF.close();
} catch (IOException e) {
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
}
}
@Override
@SuppressLint("LongLogTag")
public void run() {
boolean done;
@ -67,6 +57,7 @@ public class DownloadRunnableFallback extends Thread {
try {
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
int mId = 1;
mConn = mMission.openConnection(mId, rangeStart, -1);
mMission.establishConnection(mId, mConn);
@ -81,7 +72,7 @@ public class DownloadRunnableFallback extends Thread {
if (!mMission.unknownLength)
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
mF = mMission.storage.getStream();
mF.seek(mMission.offsets[mMission.current] + start);
mIs = mConn.getInputStream();
@ -110,6 +101,10 @@ public class DownloadRunnableFallback extends Thread {
return;
}
if (DEBUG) {
Log.e(TAG, "got exception, retrying...", e);
}
run();// try again
return;
}

View File

@ -1,16 +1,18 @@
package us.shandian.giga.get;
import android.support.annotation.NonNull;
public class FinishedMission extends Mission {
public FinishedMission() {
}
public FinishedMission(DownloadMission mission) {
public FinishedMission(@NonNull DownloadMission mission) {
source = mission.source;
length = mission.length;// ¿or mission.done?
timestamp = mission.timestamp;
name = mission.name;
location = mission.location;
kind = mission.kind;
storage = mission.storage;
}
}

View File

@ -1,12 +1,14 @@
package us.shandian.giga.get;
import java.io.File;
import android.support.annotation.NonNull;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import us.shandian.giga.io.StoredFileHelper;
public abstract class Mission implements Serializable {
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
/**
* Source url of the resource
@ -23,33 +25,24 @@ public abstract class Mission implements Serializable {
*/
public long timestamp;
/**
* The filename
*/
public String name;
/**
* The directory to store the download
*/
public String location;
/**
* pre-defined content type
*/
public char kind;
/**
* get the target file on the storage
*
* @return File object
* The downloaded file
*/
public File getDownloadedFile() {
return new File(location, name);
}
public StoredFileHelper storage;
/**
* Delete the downloaded file
*
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
*/
public boolean delete() {
deleted = true;
return getDownloadedFile().delete();
if (storage != null) return storage.delete();
return true;
}
/**
@ -57,10 +50,11 @@ public abstract class Mission implements Serializable {
*/
public transient boolean deleted = false;
@NonNull
@Override
public String toString() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
}
}

View File

@ -1,73 +0,0 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME;
public class DownloadDataSource {
private static final String TAG = "DownloadDataSource";
private final DownloadMissionHelper downloadMissionHelper;
public DownloadDataSource(Context context) {
downloadMissionHelper = new DownloadMissionHelper(context);
}
public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = downloadMissionHelper.getReadableDatabase();
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
null, null, null, DownloadMissionHelper.KEY_TIMESTAMP);
int count = cursor.getCount();
if (count == 0) return new ArrayList<>(1);
ArrayList<FinishedMission> result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(DownloadMissionHelper.getMissionFromCursor(cursor));
}
return result;
}
public void addMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
database.insert(MISSIONS_TABLE_NAME, null, values);
}
public void deleteMission(Mission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
database.delete(MISSIONS_TABLE_NAME,
KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?",
new String[]{downloadMission.location, downloadMission.name});
}
public void updateMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
String whereClause = KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?";
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
whereClause, new String[]{downloadMission.location, downloadMission.name});
if (rowsAffected != 1) {
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
}

View File

@ -1,112 +0,0 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
/**
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
*/
public class DownloadMissionHelper extends SQLiteOpenHelper {
private final String TAG = "DownloadMissionHelper";
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 3;
/**
* The table name of download missions
*/
static final String MISSIONS_TABLE_NAME = "download_missions";
/**
* The key to the directory location of the mission
*/
static final String KEY_LOCATION = "location";
/**
* The key to the urls of a mission
*/
static final String KEY_SOURCE_URL = "url";
/**
* The key to the name of a mission
*/
static final String KEY_NAME = "name";
/**
* The key to the done.
*/
static final String KEY_DONE = "bytes_downloaded";
static final String KEY_TIMESTAMP = "timestamp";
static final String KEY_KIND = "kind";
/**
* The statement to create the table
*/
private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
KEY_LOCATION + " TEXT NOT NULL, " +
KEY_NAME + " TEXT NOT NULL, " +
KEY_SOURCE_URL + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " +
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
public DownloadMissionHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 2) {
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;");
}
}
/**
* Returns all values of the download mission as ContentValues.
*
* @param downloadMission the download mission
* @return the content values
*/
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE_URL, downloadMission.source);
values.put(KEY_LOCATION, downloadMission.location);
values.put(KEY_NAME, downloadMission.name);
values.put(KEY_DONE, downloadMission.done);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
return values;
}
public static FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null");
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
FinishedMission mission = new FinishedMission();
mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));;
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.kind = kind.charAt(0);
return mission;
}
}

View File

@ -0,0 +1,237 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.io.StoredFileHelper;
/**
* SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s
*/
public class FinishedMissionStore extends SQLiteOpenHelper {
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 4;
/**
* The table name of download missions (old)
*/
private static final String MISSIONS_TABLE_NAME_v2 = "download_missions";
/**
* The table name of download missions
*/
private static final String FINISHED_TABLE_NAME = "finished_missions";
/**
* The key to the urls of a mission
*/
private static final String KEY_SOURCE = "url";
/**
* The key to the done.
*/
private static final String KEY_DONE = "bytes_downloaded";
private static final String KEY_TIMESTAMP = "timestamp";
private static final String KEY_KIND = "kind";
private static final String KEY_PATH = "path";
/**
* The statement to create the table
*/
private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + FINISHED_TABLE_NAME + " (" +
KEY_PATH + " TEXT NOT NULL, " +
KEY_SOURCE + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " +
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
private Context context;
public FinishedMissionStore(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 2) {
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;");
oldVersion++;
}
if (oldVersion == 3) {
final String KEY_LOCATION = "location";
final String KEY_NAME = "name";
db.execSQL(MISSIONS_CREATE_TABLE);
Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null,
null, null, null, KEY_TIMESTAMP);
int count = cursor.getCount();
if (count > 0) {
db.beginTransaction();
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
values.put(KEY_PATH, Uri.fromFile(
new File(
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
cursor.getString(cursor.getColumnIndex(KEY_NAME))
)
).toString());
db.insert(FINISHED_TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
db.endTransaction();
}
cursor.close();
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
}
}
/**
* Returns all values of the download mission as ContentValues.
*
* @param downloadMission the download mission
* @return the content values
*/
private ContentValues getValuesOfMission(@NonNull Mission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE, downloadMission.source);
values.put(KEY_PATH, downloadMission.storage.getUri().toString());
values.put(KEY_DONE, downloadMission.length);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
return values;
}
private FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null");
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
FinishedMission mission = new FinishedMission();
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE));
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.kind = kind.charAt(0);
try {
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
} catch (Exception e) {
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
mission.storage = new StoredFileHelper(null, path, "", "");
}
return mission;
}
//////////////////////////////////
// Data source methods
///////////////////////////////////
public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = getReadableDatabase();
Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null,
null, null, null, KEY_TIMESTAMP + " DESC");
int count = cursor.getCount();
if (count == 0) return new ArrayList<>(1);
ArrayList<FinishedMission> result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(getMissionFromCursor(cursor));
}
return result;
}
public void addFinishedMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(downloadMission);
database.insert(FINISHED_TABLE_NAME, null, values);
}
public void deleteMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
String ts = String.valueOf(mission.timestamp);
SQLiteDatabase database = getWritableDatabase();
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
ts, mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission");
}
}
public void updateMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(mission);
String ts = String.valueOf(mission.timestamp);
int rowsAffected;
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission");
}
if (rowsAffected != 1) {
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
}

View File

@ -1,20 +1,22 @@
package us.shandian.giga.postprocessing.io;
package us.shandian.giga.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class ChunkFileInputStream extends SharpStream {
private RandomAccessFile source;
private SharpStream source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
source = new RandomAccessFile(file, mode);
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
this(target, start, target.length());
}
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
source = target;
offset = start;
length = end - start;
position = 0;
@ -94,23 +96,19 @@ public class ChunkFileInputStream extends SharpStream {
}
@Override
public int available() {
public long available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
public void close() {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
public boolean isClosed() {
return source == null;
}
@ -147,7 +145,4 @@ public class ChunkFileInputStream extends SharpStream {
public void write(byte[] buffer, int offset, int count) {
}
@Override
public void flush() {
}
}

View File

@ -0,0 +1,497 @@
package us.shandian.giga.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;
public class CircularFileWriter extends SharpStream {
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static int COPY_BUFFER_SIZE = 128 * 1024; // 128 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 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(SharpStream target, 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(target);
callback = checker;
reportPosition = NOTIFY_BYTES_INTERVAL;
}
private void flushAuxiliar(long amount) throws IOException {
if (aux.length < 1) {
return;
}
out.flush();
aux.flush();
boolean underflow = aux.offset < aux.length || out.offset < out.length;
byte[] buffer = new byte[COPY_BUFFER_SIZE];
aux.target.seek(0);
out.target.seek(out.length);
long length = amount;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
if (read < 1) {
amount -= length;
break;
}
out.writeProof(buffer, read);
length -= read;
}
if (underflow) {
if (out.offset >= out.length) {
// calculate the aux underflow pointer
if (aux.offset < amount) {
out.offset += aux.offset;
aux.offset = 0;
out.target.seek(out.offset);
} else {
aux.offset -= amount;
out.offset = out.length + amount;
}
} else {
aux.offset = 0;
}
} else {
out.offset += amount;
aux.offset -= amount;
}
out.length += amount;
if (out.length > maxLengthKnown) {
maxLengthKnown = out.length;
}
if (amount < aux.length) {
// move the excess data to the beginning of the file
long readOffset = amount;
long writeOffset = 0;
aux.length -= amount;
length = aux.length;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
aux.target.seek(writeOffset);
aux.writeProof(buffer, read);
writeOffset += read;
readOffset += read;
length -= read;
aux.target.seek(readOffset);
}
aux.target.setLength(aux.length);
return;
}
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(aux.length);
out.flush();
// change file length (if required)
long length = Math.max(maxLengthKnown, out.length);
if (length != out.target.length()) {
out.target.setLength(length);
}
close();
return length;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void close() {
if (out != null) {
out.close();
out = null;
}
if (aux != null) {
aux.close();
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: " + end + "<" + 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;
}
aux.write(b, off, len);
if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
flushAuxiliar(available);
}
} 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) {
// do not ignore the seek offset if a underflow exists
long relativeOffset = out.getOffset() + aux.getOffset();
if (relativeOffset == total) {
return;
}
}
// 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 isClosed() {
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 SharpStream target;
private long offset;
protected long length;
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
private int queueSize;
BufferedFile(File file) throws FileNotFoundException {
this.target = new FileStream(file);
}
BufferedFile(SharpStream target) {
this.target = target;
}
protected long getOffset() {
return offset + queueSize;// absolute offset in the file
}
protected void close() {
queue = null;
target.close();
}
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
}
}
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;
}
void reset() throws IOException {
offset = 0;
length = 0;
target.seek(0);
}
protected void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) {
return;// nothing to do
}
offset = absoluteOffset;
target.seek(absoluteOffset);
}
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 absLength;
try {
absLength = Long.toString(target.length());
} catch (IOException e) {
absLength = "[" + e.getLocalizedMessage() + "]";
}
return String.format(
"offset=%s length=%s queue=%s absLength=%s",
offset, length, queueSize, absLength
);
}
}
}

View File

@ -1,35 +1,27 @@
package us.shandian.giga.postprocessing.io;
package us.shandian.giga.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;
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";
public FileStream(@NonNull File target) throws FileNotFoundException {
this.source = new RandomAccessFile(target, "rw");
}
this.mode = mode;
source = new RandomAccessFile(path, flags);
public FileStream(@NonNull String path) throws FileNotFoundException {
this.source = new RandomAccessFile(path, "rw");
}
@Override
@ -39,7 +31,7 @@ public class FileStream extends SharpStream {
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
return source.read(b);
}
@Override
@ -49,40 +41,37 @@ public class FileStream extends SharpStream {
@Override
public long skip(long pos) throws IOException {
FileChannel fc = source.getChannel();
fc.position(fc.position() + pos);
return pos;
return source.skipBytes((int) pos);
}
@Override
public int available() {
public long available() {
try {
return (int) (source.length() - source.getFilePointer());
} catch (IOException ex) {
return source.length() - source.getFilePointer();
} catch (IOException e) {
return 0;
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
public void close() {
if (source == null) return;
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
// nothing to do
}
source = null;
}
@Override
public boolean isDisposed() {
public boolean isClosed() {
return source == null;
}
@Override
public void rewind() throws IOException {
source.getChannel().position(0);
source.seek(0);
}
@Override
@ -92,12 +81,22 @@ public class FileStream extends SharpStream {
@Override
public boolean canRead() {
return mode == Mode.Read || mode == Mode.ReadWrite;
return true;
}
@Override
public boolean canWrite() {
return mode == Mode.ReadWrite;
return true;
}
@Override
public boolean canSeek() {
return true;
}
@Override
public boolean canSetLength() {
return true;
}
@Override
@ -115,12 +114,18 @@ public class FileStream extends SharpStream {
source.write(buffer, offset, count);
}
@Override
public void flush() {
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
@Override
public void seek(long offset) throws IOException {
source.seek(offset);
}
@Override
public long length() throws IOException {
return source.length();
}
}

View File

@ -0,0 +1,145 @@
package us.shandian.giga.io;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class FileStreamSAF extends SharpStream {
private final FileInputStream in;
private final FileOutputStream out;
private final FileChannel channel;
private final ParcelFileDescriptor file;
private boolean disposed;
public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException {
// Notes:
// the file must exists first
// ¡read-write mode must allow seek!
// It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices
file = contentResolver.openFileDescriptor(fileUri, "rw");
if (file == null) {
throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString());
}
in = new FileInputStream(file.getFileDescriptor());
out = new FileOutputStream(file.getFileDescriptor());
channel = out.getChannel();// or use in.getChannel()
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] buffer) throws IOException {
return in.read(buffer);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
return in.read(buffer, offset, count);
}
@Override
public long skip(long amount) throws IOException {
return in.skip(amount);// ¿or use channel.position(channel.position() + amount)?
}
@Override
public long available() {
try {
return in.available();
} catch (IOException e) {
return 0;// ¡but not -1!
}
}
@Override
public void rewind() throws IOException {
seek(0);
}
@Override
public void close() {
try {
disposed = true;
file.close();
in.close();
out.close();
channel.close();
} catch (IOException e) {
Log.e("FileStreamSAF", "close() error", e);
}
}
@Override
public boolean isClosed() {
return disposed;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
public boolean canSetLength() {
return true;
}
public boolean canSeek() {
return true;
}
@Override
public void write(byte value) throws IOException {
out.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
out.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
out.write(buffer, offset, count);
}
public void setLength(long length) throws IOException {
channel.truncate(length);
}
public void seek(long offset) throws IOException {
channel.position(offset);
}
@Override
public long length() throws IOException {
return channel.size();
}
}

View File

@ -3,7 +3,7 @@
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.postprocessing.io;
package us.shandian.giga.io;
import android.support.annotation.NonNull;
@ -14,6 +14,7 @@ import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
*
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
@ -49,11 +50,12 @@ public class SharpInputStream extends InputStream {
@Override
public int available() {
return base.available();
long res = base.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override
public void close() {
base.dispose();
base.close();
}
}

View File

@ -0,0 +1,289 @@
package us.shandian.giga.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.provider.DocumentFile;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
public class StoredDirectoryHelper {
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
private File ioTree;
private DocumentFile docTree;
private Context context;
private String tag;
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
this.ioTree = new File(URI.create(path.toString()));
return;
}
this.context = context;
try {
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
} catch (Exception e) {
throw new IOException(e);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
throw new IOException("Storage Access Framework with Directory API is not available");
this.docTree = DocumentFile.fromTreeUri(context, path);
if (this.docTree == null)
throw new IOException("Failed to create the tree from Uri");
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location);
this.tag = tag;
}
public StoredFileHelper createFile(String filename, String mime) {
return createFile(filename, mime, false);
}
public StoredFileHelper createUniqueFile(String name, String mime) {
ArrayList<String> matches = new ArrayList<>();
String[] filename = splitFilename(name);
String lcFilename = filename[0].toLowerCase();
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
for (File file : ioTree.listFiles())
addIfStartWith(matches, lcFilename, file.getName());
} else {
// warning: SAF file listing is very slow
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
);
String[] projection = new String[]{COLUMN_DISPLAY_NAME};
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
if (cursor != null) {
while (cursor.moveToNext())
addIfStartWith(matches, lcFilename, cursor.getString(0));
}
}
}
if (matches.size() < 1) {
return createFile(name, mime, true);
} else {
// check if the filename is in use
String lcName = name.toLowerCase();
for (String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
}
}
// check if not in use
if (lcName != null) return createFile(name, mime, true);
}
Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
}
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
}
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
StoredFileHelper storage;
try {
if (docTree == null)
storage = new StoredFileHelper(ioTree, filename, mime);
else
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
} catch (IOException e) {
return null;
}
storage.tag = tag;
return storage;
}
public Uri getUri() {
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
}
public boolean exists() {
return docTree == null ? ioTree.exists() : docTree.exists();
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
return docTree == null;
}
/**
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
* necessary but nonexistent parent directories. Note that if this
* operation fails it may have succeeded in creating some of the necessary
* parent directories.
*
* @return <code>true</code> if and only if the directory was created,
* along with all necessary parent directories or already exists; <code>false</code>
* otherwise
*/
public boolean mkdirs() {
if (docTree == null) {
return ioTree.exists() || ioTree.mkdirs();
}
if (docTree.exists()) return true;
try {
DocumentFile parent;
String child = docTree.getName();
while (true) {
parent = docTree.getParentFile();
if (parent == null || child == null) break;
if (parent.exists()) return true;
parent.createDirectory(child);
child = parent.getName();// for the next iteration
}
} catch (Exception e) {
// no more parent directories or unsupported by the storage provider
}
return false;
}
public String getTag() {
return tag;
}
public Uri findFile(String filename) {
if (docTree == null) {
File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null;
}
DocumentFile res = findFileSAFHelper(context, docTree, filename);
return res == null ? null : res.getUri();
}
public boolean canWrite() {
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
}
@NonNull
@Override
public String toString() {
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
}
////////////////////
// Utils
///////////////////
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
if (str == null || str.isEmpty()) return;
str = str.toLowerCase();
if (str.startsWith(base)) list.add(str);
}
private static String[] splitFilename(@NonNull String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
return new String[]{filename, ""};
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
}
private static String makeFileName(String name, int idx, String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
}
/**
* Fast (but not enough) file/directory finder under the storage access framework
*
* @param context The context
* @param tree Directory where search
* @param filename Target filename
* @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null
*/
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return tree.findFile(filename);// warning: this is very slow
}
if (!tree.canRead()) return null;// missing read permission
final int name = 0;
final int documentId = 1;
// LOWER() SQL function is not supported
String selection = COLUMN_DISPLAY_NAME + " = ?";
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
);
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
ContentResolver contentResolver = context.getContentResolver();
filename = filename.toLowerCase();
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
if (cursor == null) return null;
while (cursor.moveToNext()) {
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
continue;
return DocumentFile.fromSingleUri(
context, DocumentsContract.buildDocumentUriUsingTree(
tree.getUri(), cursor.getString(documentId)
)
);
}
}
return null;
}
}

View File

@ -0,0 +1,381 @@
package us.shandian.giga.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.provider.DocumentFile;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
public class StoredFileHelper implements Serializable {
private static final long serialVersionUID = 0L;
public static final String DEFAULT_MIME = "application/octet-stream";
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient Context context;
protected String source;
private String sourceTree;
protected String tag;
private String srcName;
private String srcType;
public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) this.sourceTree = parent.toString();
this.tag = tag;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
this.docTree = tree;
this.context = context;
DocumentFile res;
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) throw new IOException("Cannot create the file");
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
StoredFileHelper(File location, String filename, String mime) throws IOException {
this.ioFile = new File(location, filename);
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete())
throw new IOException("The filename is already in use by non-file entity and cannot overwrite it");
} else {
if (!this.ioFile.createNewFile())
throw new IOException("Cannot create the file");
}
this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) throw new RuntimeException("SAF not available");
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
this.docTree = DocumentFile.fromTreeUri(context, parent);
this.sourceTree = parent.toString();
}
this.srcName = getName();
this.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid())
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) instance.srcName = storage.srcName;
if (instance.srcType == null) instance.srcType = storage.srcType;
return instance;
}
public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) {
// SAF notes:
// ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files
// ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(mime)
.putExtra(Intent.EXTRA_TITLE, filename)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS)
.putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks
who.startActivityForResult(intent, requestCode);
}
public SharpStream getStream() throws IOException {
invalid();
if (docFile == null)
return new FileStream(ioFile);
else
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
invalid();
return docFile == null;
}
public boolean isInvalid() {
return source == null;
}
public Uri getUri() {
invalid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
}
public Uri getParentUri() {
invalid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
invalid();
try (SharpStream fs = getStream()) {
fs.setLength(0);
}
}
public boolean delete() {
if (source == null) return true;
if (docFile == null) return ioFile.delete();
boolean res = docFile.delete();
try {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (Exception ex) {
// nothing to do
}
return res;
}
public long length() {
invalid();
return docFile == null ? ioFile.length() : docFile.length();
}
public boolean canWrite() {
if (source == null) return false;
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public String getName() {
if (source == null)
return srcName;
else if (docFile == null)
return ioFile.getName();
String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null || docFile == null)
return srcType;
String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
return tag;
}
public boolean existsAsFile() {
if (source == null) return false;
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
return exists && isFile;
}
public boolean create() {
invalid();
boolean result;
if (docFile == null) {
try {
result = ioFile.createNewFile();
} catch (IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) return false;
try {
docFile = createSAF(context, srcType, srcName);
if (docFile == null || docFile.getName() == null) return false;
result = true;
} catch (IOException e) {
return false;
}
}
if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
return result;
}
public void invalidate() {
if (source == null) return;
srcName = getName();
srcType = getType();
source = null;
docTree = null;
docFile = null;
ioFile = null;
context = null;
}
public boolean equals(StoredFileHelper storage) {
if (this == storage) return true;
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
return false;
if (this.isInvalid() || storage.isInvalid()) {
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) return false;
if (this.isDirect())
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
return DocumentsContract.getDocumentId(
this.docFile.getUri()
).equalsIgnoreCase(DocumentsContract.getDocumentId(
storage.docFile.getUri()
));
}
@NonNull
@Override
public String toString() {
if (source == null)
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
else
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
}
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (Exception e) {
if (docFile.getName() == null) throw new IOException(e);
}
}
private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete())
throw new IOException("Directory with the same name found but cannot delete");
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) throw new IOException("Cannot create the file");
}
return res;
}
private String getLowerCase(String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(String str1, String str2) {
if (str1 == null && str2 == null) return false;
if ((str1 == null) != (str2 == null)) return true;
return !str1.equals(str2);
}
}

View File

@ -0,0 +1,41 @@
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;
class M4aNoDash extends Postprocessing {
M4aNoDash() {
super(false, true, ALGORITHM_M4A_NO_DASH);
}
@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,24 +1,22 @@
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 java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class Mp4DashMuxer extends Postprocessing {
class Mp4FromDashMuxer extends Postprocessing {
Mp4DashMuxer(DownloadMission mission) {
super(mission, 15360 * 1024/* 15 MiB */, true);
Mp4FromDashMuxer() {
super(true, true, ALGORITHM_MP4_FROM_DASH_MUXER);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4DashWriter muxer = new Mp4DashWriter(sources);
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
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,140 +1,214 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
import us.shandian.giga.postprocessing.io.CircularFile;
import us.shandian.giga.io.ChunkFileInputStream;
import us.shandian.giga.io.CircularFileWriter;
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
import us.shandian.giga.service.DownloadManagerService;
public abstract class Postprocessing {
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;
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
public abstract class Postprocessing implements Serializable {
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";
static transient final byte OK_RESULT = ERROR_NOTHING;
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) {
throw new NullPointerException("algorithmName");
} else switch (algorithmName) {
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
Postprocessing instance;
switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
return new TtmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission);
case ALGORITHM_MP4_MUXER:
return new Mp4Muxer(mission);
instance = new TtmlConverter();
break;
case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission);
instance = new WebMMuxer();
break;
case ALGORITHM_MP4_FROM_DASH_MUXER:
instance = new Mp4FromDashMuxer();
break;
case ALGORITHM_M4A_NO_DASH:
instance = new M4aNoDash();
break;
/*case "example-algorithm":
return new ExampleAlgorithm(mission);*/
instance = new ExampleAlgorithm();*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
throw new UnsupportedOperationException("Unimplemented post-processing algorithm: " + algorithmName);
}
instance.args = args;
return instance;
}
/**
* Get a boolean value that indicate if the given algorithm work on the same
* file
*/
public boolean worksOnSameFile;
public final boolean worksOnSameFile;
/**
* Get the recommended space to reserve for the given algorithm. The amount
* is in bytes
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
*/
public int recommendedReserve;
public final boolean reserveSpace;
/**
* the download to post-process
* Gets the given algorithm short name
*/
protected DownloadMission mission;
private final String name;
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
this.mission = mission;
this.recommendedReserve = recommendedReserve;
private String[] args;
protected transient DownloadMission mission;
private File tempFile;
Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) {
this.reserveSpace = reserveSpace;
this.worksOnSameFile = worksOnSameFile;
this.name = algorithmName;// for debugging only
}
public void run() throws IOException {
File file = mission.getDownloadedFile();
CircularFile out = null;
public void setTemporalDir(@NonNull File directory) {
long rnd = (int) (Math.random() * 100000f);
tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp");
}
public void cleanupTemporalDir() {
if (tempFile != null && tempFile.exists()) {
//noinspection ResultOfMethodCallIgnored
tempFile.delete();
}
}
public void run(DownloadMission target) throws IOException {
this.mission = target;
CircularFileWriter out = null;
int result;
long finalLength = -1;
mission.done = 0;
mission.length = file.length();
mission.length = mission.storage.length();
if (worksOnSameFile) {
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try {
int i = 0;
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
if (test(sources)) {
for (SharpStream source : sources) source.rewind();
OffsetChecker checker = () -> {
for (ChunkFileInputStream source : sources) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
* or the CircularFileWriter can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
if (source.isClosed() || source.available() < 1) {
continue;// the selected source is not used anymore
}
return sources[idx[0]].getFilePointer() - 1;
return source.getFilePointer() - 1;
}
return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
out.onProgress = this::progressReport;
out.onWriteError = (err) -> {
mission.psState = 3;
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
try {
synchronized (this) {
while (mission.psState == 3)
wait();
}
} catch (InterruptedException e) {
// nothing to do
Log.e(this.getClass().getSimpleName(), "got InterruptedException");
}
return mission.errCode == ERROR_NOTHING;
};
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} else {
result = OK_RESULT;
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
if (source != null && !source.isClosed()) {
source.close();
}
}
if (out != null) {
out.dispose();
out.close();
}
if (tempFile != null) {
//noinspection ResultOfMethodCallIgnored
tempFile.delete();
tempFile = null;
}
}
} else {
result = process(null);
result = test() ? process(null) : OK_RESULT;
}
if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length();
if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength;
}
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
this.mission = null;
}
/**
* Abstract method to execute the pos-processing algorithm
* Test if the post-processing algorithm can be skipped
*
* @param sources files to be processed
* @return {@code true} if the post-processing is required, otherwise, {@code false}
* @throws IOException if an I/O error occurs.
*/
boolean test(SharpStream... sources) throws IOException {
return true;
}
/**
* Abstract method to execute the post-processing algorithm
*
* @param out output stream
* @param sources files to be processed
@ -144,14 +218,14 @@ public abstract class Postprocessing {
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
String getArgumentAt(int index, String defaultValue) {
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
if (args == null || index >= args.length) {
return defaultValue;
}
return mission.postprocessingArgs[index];
return args[index];
}
void progressReport(long done) {
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;
@ -161,4 +235,22 @@ public abstract class Postprocessing {
mission.mHandler.sendMessage(m);
}
@NonNull
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("name=").append(name).append('[');
if (args != null) {
for (String arg : args) {
str.append(", ");
str.append(arg);
}
str.delete(0, 1);
}
return str.append(']').toString();
}
}

View File

@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import org.schabi.newpipe.streams.SubtitleConverter;
import org.schabi.newpipe.streams.io.SharpStream;
import org.xml.sax.SAXException;
import java.io.IOException;
@ -12,18 +12,15 @@ import java.text.ParseException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.SharpInputStream;
/**
* @author kapodamy
*/
class TtmlConverter extends Postprocessing {
private static final String TAG = "TtmlConverter";
TtmlConverter(DownloadMission mission) {
TtmlConverter() {
// due how XmlPullParser works, the xml is fully loaded on the ram
super(mission, 0, true);
super(false, true, ALGORITHM_TTML_CONVERTER);
}
@Override

View File

@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class WebMMuxer extends Postprocessing {
WebMMuxer(DownloadMission mission) {
super(mission, 2048 * 1024/* 2 MiB */, true);
WebMMuxer() {
super(true, true, ALGORITHM_WEBM_MUXER);
}
@Override
@ -24,16 +22,20 @@ class WebMMuxer extends Postprocessing {
muxer.parseSources();
// youtube uses a webm with a fake video track that acts as a "cover image"
WebMTrack[] tracks = muxer.getTracksFromSource(1);
int audioTrackIndex = 0;
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].kind == TrackKind.Audio) {
audioTrackIndex = i;
int[] indexes = new int[sources.length];
for (int i = 0; i < sources.length; i++) {
WebMTrack[] tracks = muxer.getTracksFromSource(i);
for (int j = 0; j < tracks.length; j++) {
if (tracks[j].kind == TrackKind.Audio) {
indexes[i] = j;
i = sources.length;
break;
}
}
}
muxer.selectTracks(0, audioTrackIndex);
muxer.selectTracks(indexes);
muxer.build(out);
return OK_RESULT;

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

@ -13,14 +13,15 @@ import org.schabi.newpipe.R;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.get.sqlite.DownloadDataSource;
import us.shandian.giga.get.sqlite.FinishedMissionStore;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -28,13 +29,16 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManager {
private static final String TAG = DownloadManager.class.getSimpleName();
enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating}
enum NetworkState {Unavailable, Operating, MeteredOperating}
public final static int SPECIAL_NOTHING = 0;
public final static int SPECIAL_PENDING = 1;
public final static int SPECIAL_FINISHED = 2;
private final DownloadDataSource mDownloadDataSource;
static final String TAG_AUDIO = "audio";
static final String TAG_VIDEO = "video";
private final FinishedMissionStore mFinishedMissionStore;
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
private final ArrayList<FinishedMission> mMissionsFinished;
@ -45,7 +49,12 @@ public class DownloadManager {
private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
int mPrefMaxRetry;
boolean mPrefCrossNetwork;
boolean mPrefMeteredDownloads;
boolean mPrefQueueLimit;
private boolean mSelfMissionsControl;
StoredDirectoryHelper mMainStorageAudio;
StoredDirectoryHelper mMainStorageVideo;
/**
* Create a new instance
@ -53,13 +62,15 @@ public class DownloadManager {
* @param context Context for the data source for finished downloads
* @param handler Thread required for Messaging
*/
DownloadManager(@NonNull Context context, Handler handler) {
DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) {
if (DEBUG) {
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
}
mDownloadDataSource = new DownloadDataSource(context);
mFinishedMissionStore = new FinishedMissionStore(context);
mHandler = handler;
mMainStorageAudio = storageAudio;
mMainStorageVideo = storageVideo;
mMissionsFinished = loadFinishedMissions();
mPendingMissionsDir = getPendingDir(context);
@ -67,7 +78,7 @@ public class DownloadManager {
throw new RuntimeException("failed to create pending_downloads in data directory");
}
loadPendingMissions();
loadPendingMissions(context);
}
private static File getPendingDir(@NonNull Context context) {
@ -88,29 +99,24 @@ public class DownloadManager {
* Loads finished missions from the data source
*/
private ArrayList<FinishedMission> loadFinishedMissions() {
ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions();
ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions();
// missions always is stored by creation order, simply reverse the list
ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size());
// check if the files exists, otherwise, forget the download
for (int i = finishedMissions.size() - 1; i >= 0; i--) {
FinishedMission mission = finishedMissions.get(i);
File file = mission.getDownloadedFile();
if (!file.isFile()) {
if (DEBUG) {
Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath());
if (!mission.storage.existsAsFile()) {
if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName());
mFinishedMissionStore.deleteMission(mission);
finishedMissions.remove(i);
}
mDownloadDataSource.deleteMission(mission);
continue;
}
result.add(mission);
return finishedMissions;
}
return result;
}
private void loadPendingMissions() {
private void loadPendingMissions(Context ctx) {
File[] subs = mPendingMissionsDir.listFiles();
if (subs == null) {
@ -125,109 +131,76 @@ public class DownloadManager {
}
for (File sub : subs) {
if (sub.isFile()) {
DownloadMission mis = Utility.readFromFile(sub);
if (!sub.isFile()) continue;
if (mis == null) {
//noinspection ResultOfMethodCallIgnored
sub.delete();
} else {
if (mis.isFinished()) {
DownloadMission mis = Utility.readFromFile(sub);
if (mis == null || mis.isFinished()) {
//noinspection ResultOfMethodCallIgnored
sub.delete();
continue;
}
File dl = mis.getDownloadedFile();
boolean exists = dl.exists();
boolean exists;
try {
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
} catch (Exception ex) {
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex);
mis.storage.invalidate();
exists = false;
}
if (mis.isPsRunning()) {
if (mis.postprocessingThis) {
if (mis.psAlgorithm.worksOnSameFile) {
// Incomplete post-processing results in a corrupted download file
// because the selected algorithm works on the same file to save space.
if (exists && dl.isFile() && !dl.delete())
// the file will be deleted if the storage API
// is Java IO (avoid showing the "Save as..." dialog)
if (exists && mis.storage.isDirect() && !mis.storage.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
}
mis.postprocessingState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
mis.errObject = new RuntimeException("stopped unexpectedly");
} else if (exists && !dl.isFile()) {
// probably a folder, this should never happens
if (!sub.delete()) {
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
}
continue;
mis.psState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
} else if (!exists) {
tryRecover(mis);
// the progress is lost, reset mission state
if (mis.isInitialized())
mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST);
}
if (!exists) {
// downloaded file deleted, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount;
m.source = mis.source;
m.maxRetry = mis.maxRetry;
m.nearLength = mis.nearLength;
mis = m;
if (mis.psAlgorithm != null) {
mis.psAlgorithm.cleanupTemporalDir();
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
}
mis.running = false;
mis.recovered = exists;
mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler;
mMissionsPending.add(mis);
}
}
}
if (mMissionsPending.size() > 1) {
if (mMissionsPending.size() > 1)
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
}
}
/**
* Start a new download mission
*
* @param urls the list of urls to download
* @param location the location
* @param name the name of the file to create
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
* @param threads the number of threads maximal used to download chunks of the file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param source source url of the resource
* @param psArgs the arguments for the post-processing algorithm.
* @param mission the new download mission to add and run (if possible)
*/
void startMission(String[] urls, String location, String name, char kind, int threads,
String source, String psName, String[] psArgs, long nearLength) {
void startMission(DownloadMission mission) {
synchronized (this) {
// check for existing pending download
DownloadMission pendingMission = getPendingMission(location, name);
if (pendingMission != null) {
// generate unique filename (?)
try {
name = generateUniqueName(location, name);
} catch (Exception e) {
Log.e(TAG, "Unable to generate unique name", e);
name = System.currentTimeMillis() + name;
Log.i(TAG, "Using " + name);
}
} else {
// check for existing finished download
int index = getFinishedMissionIndex(location, name);
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
}
DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs);
mission.timestamp = System.currentTimeMillis();
mission.threadCount = threads;
mission.source = source;
mission.mHandler = mHandler;
mission.maxRetry = mPrefMaxRetry;
mission.nearLength = nearLength;
// create metadata file
while (true) {
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
@ -242,14 +215,25 @@ public class DownloadManager {
mission.timestamp = System.currentTimeMillis();
}
mSelfMissionsControl = true;
mMissionsPending.add(mission);
// Before starting, save the state in case the internet connection is not available
// Before continue, save the metadata in case the internet connection is not available
Utility.writeToFile(mission.metadata, mission);
if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) {
if (mission.storage == null) {
// noting to do here
mission.errCode = DownloadMission.ERROR_FILE_CREATION;
if (mission.errObject != null)
mission.errObject = new IOException("DownloadMission.storage == NULL");
return;
}
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
if (canDownloadInCurrentNetwork() && start) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
}
}
}
@ -257,13 +241,14 @@ public class DownloadManager {
public void resumeMission(DownloadMission mission) {
if (!mission.running) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
}
}
public void pauseMission(DownloadMission mission) {
if (mission.running) {
mission.setEnqueued(false);
mission.pause();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
@ -275,7 +260,7 @@ public class DownloadManager {
mMissionsPending.remove(mission);
} else if (mission instanceof FinishedMission) {
mMissionsFinished.remove(mission);
mDownloadDataSource.deleteMission(mission);
mFinishedMissionStore.deleteMission(mission);
}
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
@ -283,18 +268,54 @@ public class DownloadManager {
}
}
public void forgetMission(StoredFileHelper storage) {
synchronized (this) {
Mission mission = getAnyMission(storage);
if (mission == null) return;
if (mission instanceof DownloadMission) {
mMissionsPending.remove(mission);
} else if (mission instanceof FinishedMission) {
mMissionsFinished.remove(mission);
mFinishedMissionStore.deleteMission(mission);
}
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
mission.storage = null;
mission.delete();
}
}
public void tryRecover(DownloadMission mission) {
StoredDirectoryHelper mainStorage = getMainStorage(mission.storage.getTag());
if (!mission.storage.isInvalid() && mission.storage.create()) return;
// using javaIO cannot recreate the file
// using SAF in older devices (no tree available)
//
// force the user to pick again the save path
mission.storage.invalidate();
if (mainStorage == null) return;
// if the user has changed the save path before this download, the original save path will be lost
StoredFileHelper newStorage = mainStorage.createFile(mission.storage.getName(), mission.storage.getType());
if (newStorage != null) mission.storage = newStorage;
}
/**
* Get a pending mission by its location and name
* Get a pending mission by its path
*
* @param location the location
* @param name the name
* @param storage where the file possible is stored
* @return the mission or null if no such mission exists
*/
@Nullable
private DownloadMission getPendingMission(String location, String name) {
private DownloadMission getPendingMission(StoredFileHelper storage) {
for (DownloadMission mission : mMissionsPending) {
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
if (mission.storage.equals(storage)) {
return mission;
}
}
@ -302,16 +323,14 @@ public class DownloadManager {
}
/**
* Get a finished mission by its location and name
* Get a finished mission by its path
*
* @param location the location
* @param name the name
* @param storage where the file possible is stored
* @return the mission index or -1 if no such mission exists
*/
private int getFinishedMissionIndex(String location, String name) {
private int getFinishedMissionIndex(StoredFileHelper storage) {
for (int i = 0; i < mMissionsFinished.size(); i++) {
FinishedMission mission = mMissionsFinished.get(i);
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
if (mMissionsFinished.get(i).storage.equals(storage)) {
return i;
}
}
@ -319,12 +338,12 @@ public class DownloadManager {
return -1;
}
public Mission getAnyMission(String location, String name) {
private Mission getAnyMission(StoredFileHelper storage) {
synchronized (this) {
Mission mission = getPendingMission(location, name);
Mission mission = getPendingMission(storage);
if (mission != null) return mission;
int idx = getFinishedMissionIndex(location, name);
int idx = getFinishedMissionIndex(storage);
if (idx >= 0) return mMissionsFinished.get(idx);
}
@ -335,7 +354,7 @@ public class DownloadManager {
int count = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running && !mission.isFinished() && !mission.isPsFailed())
if (mission.running && !mission.isPsFailed() && !mission.isFinished())
count++;
}
}
@ -343,62 +362,36 @@ public class DownloadManager {
return count;
}
void pauseAllMissions() {
public void pauseAllMissions(boolean force) {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) mission.pause();
for (DownloadMission mission : mMissionsPending) {
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
if (force) mission.threads = null;// avoid waiting for threads
mission.pause();
flag = true;
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
/**
* Splits the filename into name and extension
* <p>
* Dots are ignored if they appear: not at all, at the beginning of the file,
* at the end of the file
*
* @param name the name to split
* @return a string array with a length of 2 containing the name and the extension
*/
private static String[] splitName(String name) {
int dotIndex = name.lastIndexOf('.');
if (dotIndex <= 0 || (dotIndex == name.length() - 1)) {
return new String[]{name, ""};
} else {
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
public void startAllMissions() {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running || mission.isCorrupt()) continue;
flag = true;
mission.start();
}
}
/**
* Generates a unique file name.
* <p>
* e.g. "myName (1).txt" if the name "myName.txt" exists.
*
* @param location the location (to check for existing files)
* @param name the name of the file
* @return the unique file name
* @throws IllegalArgumentException if the location is not a directory
* @throws SecurityException if the location is not readable
*/
private static String generateUniqueName(String location, String name) {
if (location == null) throw new NullPointerException("location is null");
if (name == null) throw new NullPointerException("name is null");
File destination = new File(location);
if (!destination.isDirectory()) {
throw new IllegalArgumentException("location is not a directory: " + location);
}
final String[] nameParts = splitName(name);
String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0]));
Arrays.sort(existingName);
String newName;
int downloadIndex = 0;
do {
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
++downloadIndex;
if (downloadIndex == 1000) { // Probably an error on our side
throw new RuntimeException("Too many existing files");
}
} while (Arrays.binarySearch(existingName, newName) >= 0);
return newName;
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
}
/**
@ -410,36 +403,41 @@ public class DownloadManager {
synchronized (this) {
mMissionsPending.remove(mission);
mMissionsFinished.add(0, new FinishedMission(mission));
mDownloadDataSource.addMission(mission);
mFinishedMissionStore.addFinishedMission(mission);
}
}
/**
* runs another mission in queue if possible
* runs one or multiple missions in from queue if possible
*
* @return true if exits pending missions running or a mission was started, otherwise, false
* @return true if one or multiple missions are running, otherwise, false
*/
boolean runAnotherMission() {
boolean runMissions() {
synchronized (this) {
if (mMissionsPending.size() < 1) return false;
int i = getRunningMissionsCount();
if (i > 0) return true;
if (!canDownloadInCurrentNetwork()) return false;
for (DownloadMission mission : mMissionsPending) {
if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) {
resumeMission(mission);
return true;
}
if (mPrefQueueLimit) {
for (DownloadMission mission : mMissionsPending)
if (!mission.isFinished() && mission.running) return true;
}
return false;
boolean flag = false;
for (DownloadMission mission : mMissionsPending) {
if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage())
continue;
resumeMission(mission);
if (mPrefQueueLimit) return true;
flag = true;
}
return flag;
}
}
public MissionIterator getIterator() {
mSelfMissionsControl = true;
return new MissionIterator();
}
@ -449,7 +447,7 @@ public class DownloadManager {
public void forgetFinishedDownloads() {
synchronized (this) {
for (FinishedMission mission : mMissionsFinished) {
mDownloadDataSource.deleteMission(mission);
mFinishedMissionStore.deleteMission(mission);
}
mMissionsFinished.clear();
}
@ -457,31 +455,43 @@ public class DownloadManager {
private boolean canDownloadInCurrentNetwork() {
if (mLastNetworkStatus == NetworkState.Unavailable) return false;
return !(mPrefCrossNetwork && mLastNetworkStatus == NetworkState.MobileOperating);
return !(mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating);
}
void handleConnectivityChange(NetworkState currentStatus) {
void handleConnectivityState(NetworkState currentStatus, boolean updateOnly) {
if (currentStatus == mLastNetworkStatus) return;
mLastNetworkStatus = currentStatus;
if (currentStatus == NetworkState.Unavailable) return;
if (currentStatus == NetworkState.Unavailable) {
return;
} else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) {
return;
if (!mSelfMissionsControl || updateOnly) {
return;// don't touch anything without the user interaction
}
boolean flag = false;
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
int running = 0;
int paused = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
flag = true;
if (mission.isCorrupt() || mission.isPsRunning()) continue;
if (mission.running && isMetered) {
paused++;
mission.pause();
} else if (!mission.running && !isMetered && mission.enqueued) {
running++;
mission.start();
if (mPrefQueueLimit) break;
}
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
if (running > 0) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
return;
}
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
void updateMaximumAttempts() {
@ -506,21 +516,46 @@ public class DownloadManager {
), Toast.LENGTH_LONG).show();
}
void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) {
boolean listed;
boolean finished = false;
public MissionState checkForExistingMission(StoredFileHelper storage) {
synchronized (this) {
DownloadMission mission = getPendingMission(location, name);
if (mission != null) {
listed = true;
DownloadMission pending = getPendingMission(storage);
if (pending == null) {
if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished;
} else {
listed = getFinishedMissionIndex(location, name) >= 0;
finished = listed;
if (pending.isFinished()) {
return MissionState.Finished;// this never should happen (race-condition)
} else {
return pending.running ? MissionState.PendingRunning : MissionState.Pending;
}
}
}
check.callback(listed, finished);
return MissionState.None;
}
private static boolean isDirectoryAvailable(File directory) {
return directory != null && directory.canWrite() && directory.exists();
}
static File pickAvailableTemporalDir(@NonNull Context ctx) {
if (isDirectoryAvailable(ctx.getExternalFilesDir(null)))
return ctx.getExternalFilesDir(null);
else if (isDirectoryAvailable(ctx.getFilesDir()))
return ctx.getFilesDir();
// this never should happen
return ctx.getDir("tmp", Context.MODE_PRIVATE);
}
@Nullable
private StoredDirectoryHelper getMainStorage(@NonNull String tag) {
if (tag.equals(TAG_AUDIO)) return mMainStorageAudio;
if (tag.equals(TAG_VIDEO)) return mMainStorageVideo;
Log.w(TAG, "Unknown download category, not [audio video]: " + tag);
return null;// this never should happen
}
public class MissionIterator extends DiffUtil.Callback {
@ -592,39 +627,6 @@ public class DownloadManager {
return SPECIAL_NOTHING;
}
public MissionItem getItemUnsafe(int position) {
synchronized (DownloadManager.this) {
int count = mMissionsPending.size();
int count2 = mMissionsFinished.size();
if (count > 0) {
position--;
if (position == -1)
return new MissionItem(SPECIAL_PENDING);
else if (position < count)
return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position));
else if (position == count && count2 > 0)
return new MissionItem(SPECIAL_FINISHED);
else
position -= count;
} else {
if (count2 > 0 && position == 0) {
return new MissionItem(SPECIAL_FINISHED);
}
}
position--;
if (count2 < 1) {
throw new RuntimeException(
String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position)
);
}
return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position));
}
}
public void start() {
current = getSpecialItems();
@ -647,6 +649,32 @@ public class DownloadManager {
return hasFinished;
}
/**
* Check if exists missions running and paused. Corrupted and hidden missions are not counted
*
* @return two-dimensional array contains the current missions state.
* 1° entry: true if has at least one mission running
* 2° entry: true if has at least one mission paused
*/
public boolean[] hasValidPendingMissions() {
boolean running = false;
boolean paused = false;
synchronized (DownloadManager.this) {
for (DownloadMission mission : mMissionsPending) {
if (hidden.contains(mission) || mission.isCorrupt())
continue;
if (mission.running)
paused = true;
else
running = true;
}
}
return new boolean[]{running, paused};
}
@Override
public int getOldListSize() {
@ -665,7 +693,14 @@ public class DownloadManager {
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsTheSame(oldItemPosition, newItemPosition);
Object x = snapshot.get(oldItemPosition);
Object y = current.get(newItemPosition);
if (x instanceof Mission && y instanceof Mission) {
return ((Mission) x).storage.equals(((Mission) y).storage);
}
return false;
}
}

View File

@ -1,21 +1,22 @@
package us.shandian.giga.service;
import android.Manifest;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@ -24,6 +25,9 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.PermissionChecker;
@ -36,9 +40,13 @@ import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.player.helper.LockManager;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState;
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
@ -48,7 +56,6 @@ public class DownloadManagerService extends Service {
private static final String TAG = "DownloadManagerService";
public static final int MESSAGE_RUNNING = 0;
public static final int MESSAGE_PAUSED = 1;
public static final int MESSAGE_FINISHED = 2;
public static final int MESSAGE_PROGRESS = 3;
@ -59,24 +66,25 @@ public class DownloadManagerService extends Service {
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
private DMBinder mBinder;
private DownloadManagerBinder mBinder;
private DownloadManager mManager;
private Notification mNotification;
private Handler mHandler;
private boolean mForeground = false;
private NotificationManager notificationManager = null;
private NotificationManager mNotificationManager = null;
private boolean mDownloadNotificationEnable = true;
private int downloadDoneCount = 0;
@ -85,7 +93,9 @@ public class DownloadManagerService extends Service {
private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1);
private BroadcastReceiver mNetworkStateListener;
private ConnectivityManager mConnectivityManager;
private BroadcastReceiver mNetworkStateListener = null;
private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null;
private SharedPreferences mPrefs = null;
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange;
@ -106,10 +116,10 @@ public class DownloadManagerService extends Service {
/**
* notify media scanner on downloaded media file ...
*
* @param file the downloaded file
* @param file the downloaded file uri
*/
private void notifyMediaScanner(File file) {
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
private void notifyMediaScanner(Uri file) {
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file));
}
@Override
@ -120,7 +130,7 @@ public class DownloadManagerService extends Service {
Log.d(TAG, "onCreate");
}
mBinder = new DMBinder();
mBinder = new DownloadManagerBinder();
mHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
@ -128,7 +138,9 @@ public class DownloadManagerService extends Service {
}
};
mManager = new DownloadManager(this, mHandler);
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mManager = new DownloadManager(this, mHandler, loadMainVideoStorage(), loadMainAudioStorage());
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
.setAction(Intent.ACTION_MAIN);
@ -147,54 +159,55 @@ public class DownloadManagerService extends Service {
.setContentText(getString(R.string.msg_running_detail));
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() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
handleConnectivityChange(null);
return;
}
handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO));
handleConnectivityState(false);
}
};
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network));
handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry));
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
mLock = new LockManager(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG) {
if (intent == null) {
Log.d(TAG, "Restarting");
return START_NOT_STICKY;
}
Log.d(TAG, "Starting");
Log.d(TAG, intent == null ? "Restarting" : "Starting");
}
if (intent == null) return START_NOT_STICKY;
Log.i(TAG, "Got intent: " + intent);
String action = intent.getAction();
if (action != null) {
if (action.equals(Intent.ACTION_RUN)) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
String name = intent.getStringExtra(EXTRA_NAME);
String location = intent.getStringExtra(EXTRA_LOCATION);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength));
mHandler.post(() -> startMission(intent));
} else if (downloadDoneNotification != null) {
if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) {
downloadDoneCount = 0;
@ -221,32 +234,36 @@ public class DownloadManagerService extends Service {
stopForeground(true);
if (notificationManager != null && downloadDoneNotification != null) {
if (mNotificationManager != null && downloadDoneNotification != null) {
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
}
mManager.pauseAllMissions();
manageLock(false);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL);
else
unregisterReceiver(mNetworkStateListener);
mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener);
if (icDownloadDone != null) icDownloadDone.recycle();
if (icDownloadFailed != null) icDownloadFailed.recycle();
if (icLauncher != null) icLauncher.recycle();
mManager.pauseAllMissions(true);
}
@Override
public IBinder onBind(Intent intent) {
int permissionCheck;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
}
}
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
// }
// }
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
@ -261,18 +278,19 @@ public class DownloadManagerService extends Service {
switch (msg.what) {
case MESSAGE_FINISHED:
notifyMediaScanner(mission.getDownloadedFile());
notifyFinishedDownload(mission.name);
notifyMediaScanner(mission.storage.getUri());
notifyFinishedDownload(mission.storage.getName());
mManager.setFinished(mission);
updateForegroundState(mManager.runAnotherMission());
handleConnectivityState(false);
updateForegroundState(mManager.runMissions());
break;
case MESSAGE_RUNNING:
case MESSAGE_PROGRESS:
updateForegroundState(true);
break;
case MESSAGE_ERROR:
notifyFailedDownload(mission);
updateForegroundState(mManager.runAnotherMission());
handleConnectivityState(false);
updateForegroundState(mManager.runMissions());
break;
case MESSAGE_PAUSED:
updateForegroundState(mManager.getRunningMissionsCount() > 0);
@ -293,46 +311,46 @@ public class DownloadManagerService extends Service {
}
}
private void handleConnectivityChange(NetworkInfo info) {
private void handleConnectivityState(boolean updateOnly) {
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
NetworkState status;
if (info == null) {
status = NetworkState.Unavailable;
Log.i(TAG, "actual connectivity status is unavailable");
} else if (!info.isAvailable() || !info.isConnected()) {
status = NetworkState.Unavailable;
Log.i(TAG, "actual connectivity status is not available and not connected");
} 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;
Log.i(TAG, "Active network [connectivity is unavailable]");
} else {
boolean connected = info.isConnected();
boolean metered = mConnectivityManager.isActiveNetworkMetered();
if (connected)
status = metered ? NetworkState.MeteredOperating : NetworkState.Operating;
else
status = NetworkState.Unavailable;
}
Log.i(TAG, "actual connectivity status is " + status.name());
Log.i(TAG, "Active network [connected=" + connected + " metered=" + metered + "] " + info.toString());
}
if (mManager == null) return;// avoid race-conditions while the service is starting
mManager.handleConnectivityChange(status);
mManager.handleConnectivityState(status, updateOnly);
}
private void handlePreferenceChange(SharedPreferences prefs, String key) {
private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) {
if (key.equals(getString(R.string.downloads_maximum_retry))) {
try {
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
mManager.mPrefMaxRetry = Integer.parseInt(value);
mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value);
} catch (Exception e) {
mManager.mPrefMaxRetry = 0;
}
mManager.updateMaximumAttempts();
} else if (key.equals(getString(R.string.downloads_cross_network))) {
mManager.mPrefCrossNetwork = prefs.getBoolean(key, false);
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
} else if (key.equals(getString(R.string.download_path_video_key))) {
mManager.mMainStorageVideo = loadMainVideoStorage();
} else if (key.equals(getString(R.string.download_path_audio_key))) {
mManager.mMainStorageAudio = loadMainAudioStorage();
}
}
@ -350,46 +368,78 @@ public class DownloadManagerService extends Service {
mForeground = state;
}
public static void startMission(Context context, String urls[], String location, String name, char kind,
/**
* Start a new download mission
*
* @param context the activity context
* @param urls the list of urls to download
* @param storage where the file is saved
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
* @param threads the number of threads maximal used to download chunks of the file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param source source url of the resource
* @param psArgs the arguments for the post-processing algorithm.
* @param nearLength the approximated final length of the file
*/
public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind,
int threads, String source, String psName, String[] psArgs, long nearLength) {
Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls);
intent.putExtra(EXTRA_NAME, name);
intent.putExtra(EXTRA_LOCATION, location);
intent.putExtra(EXTRA_KIND, kind);
intent.putExtra(EXTRA_THREADS, threads);
intent.putExtra(EXTRA_SOURCE, source);
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
context.startService(intent);
}
public static void checkForRunningMission(Context context, String location, String name, DMChecker check) {
Intent intent = new Intent();
intent.setClass(context, DownloadManagerService.class);
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName cname, IBinder service) {
private void startMission(Intent intent) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
Uri path = intent.getParcelableExtra(EXTRA_PATH);
Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
StoredFileHelper storage;
try {
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check);
} catch (Exception err) {
Log.w(TAG, "checkForRunningMission() callback is defective", err);
storage = new StoredFileHelper(this, parentPath, path, tag);
} catch (IOException e) {
throw new RuntimeException(e);// this never should happen
}
// 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);
}
Postprocessing ps;
if (psName == null)
ps = null;
else
ps = Postprocessing.getAlgorithm(psName, psArgs);
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, Context.BIND_AUTO_CREATE);
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads;
mission.source = source;
mission.nearLength = nearLength;
if (ps != null)
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
handleConnectivityState(true);// first check the actual network status
mManager.startMission(mission);
}
public void notifyFinishedDownload(String name) {
if (!mDownloadNotificationEnable || notificationManager == null) {
if (!mDownloadNotificationEnable || mNotificationManager == null) {
return;
}
@ -428,7 +478,7 @@ public class DownloadManagerService extends Service {
downloadDoneNotification.setContentText(downloadDoneList);
}
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
downloadDoneCount++;
}
@ -450,15 +500,15 @@ public class DownloadManagerService extends Service {
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
downloadFailedNotification.setContentTitle(getString(R.string.app_name));
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.name)));
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName())));
} else {
downloadFailedNotification.setContentTitle(getString(R.string.download_failed));
downloadFailedNotification.setContentText(mission.name);
downloadFailedNotification.setContentText(mission.storage.getName());
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
.bigText(mission.name));
.bigText(mission.storage.getName()));
}
notificationManager.notify(id, downloadFailedNotification.build());
mNotificationManager.notify(id, downloadFailedNotification.build());
}
private PendingIntent makePendingIntent(String action) {
@ -487,12 +537,66 @@ public class DownloadManagerService extends Service {
mLockAcquired = acquire;
}
// Wrapper of DownloadManager
public class DMBinder extends Binder {
private StoredDirectoryHelper loadMainVideoStorage() {
return loadMainStorage(R.string.download_path_video_key, DownloadManager.TAG_VIDEO);
}
private StoredDirectoryHelper loadMainAudioStorage() {
return loadMainStorage(R.string.download_path_audio_key, DownloadManager.TAG_AUDIO);
}
private StoredDirectoryHelper loadMainStorage(@StringRes int prefKey, String tag) {
String path = mPrefs.getString(getString(prefKey), null);
if (path == null || path.isEmpty()) return null;
if (path.charAt(0) == File.separatorChar) {
Log.i(TAG, "Old save path style present: " + path);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
path = Uri.fromFile(new File(path)).toString();
else
path = "";
mPrefs.edit().putString(getString(prefKey), "").apply();
}
try {
return new StoredDirectoryHelper(this, Uri.parse(path), tag);
} catch (Exception e) {
Log.e(TAG, "Failed to load the storage of " + tag + " from " + path, e);
Toast.makeText(this, R.string.no_available_dir, Toast.LENGTH_LONG).show();
}
return null;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Wrappers for DownloadManager
////////////////////////////////////////////////////////////////////////////////////////////////
public class DownloadManagerBinder extends Binder {
public DownloadManager getDownloadManager() {
return mManager;
}
@Nullable
public StoredDirectoryHelper getMainStorageVideo() {
return mManager.mMainStorageVideo;
}
@Nullable
public StoredDirectoryHelper getMainStorageAudio() {
return mManager.mMainStorageAudio;
}
public boolean askForSavePath() {
return DownloadManagerService.this.mPrefs.getBoolean(
DownloadManagerService.this.getString(R.string.downloads_storage_ask),
false
);
}
public void addMissionEventListener(Handler handler) {
manageObservers(handler, true);
}
@ -502,15 +606,15 @@ public class DownloadManagerService extends Service {
}
public void clearDownloadNotifications() {
if (notificationManager == null) return;
if (mNotificationManager == null) return;
if (downloadDoneNotification != null) {
notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
downloadDoneList.setLength(0);
downloadDoneCount = 0;
}
if (downloadFailedNotification != null) {
for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) {
notificationManager.cancel(downloadFailedNotificationID);
mNotificationManager.cancel(downloadFailedNotificationID);
}
mFailedDownloads.clear();
downloadFailedNotificationID++;
@ -523,8 +627,4 @@ public class DownloadManagerService extends Service {
}
public interface DMChecker {
void callback(boolean listed, boolean finished);
}
}

View File

@ -0,0 +1,5 @@
package us.shandian.giga.service;
public enum MissionState {
None, Pending, PendingRunning, Finished
}

View File

@ -8,21 +8,22 @@ import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.content.FileProvider;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -36,14 +37,20 @@ import android.widget.Toast;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
@ -57,11 +64,16 @@ import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
@ -69,6 +81,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
private static final String TAG = "MissionAdapter";
private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*";
static {
@ -85,9 +98,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
private Handler mHandler;
private MenuItem mClear;
private MenuItem mStartButton;
private MenuItem mPauseButton;
private View mEmptyMessage;
private RecoverHelper mRecover;
public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) {
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
mContext = context;
mDownloadManager = downloadManager;
mDeleter = null;
@ -105,10 +121,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
onServiceMessage(msg);
break;
}
if (mStartButton != null && mPauseButton != null) switch (msg.what) {
case DownloadManagerService.MESSAGE_DELETED:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_PAUSED:
checkMasterButtonsVisibility();
break;
}
}
};
mClear = clearButton;
mEmptyMessage = emptyMessage;
mIterator = downloadManager.getIterator();
@ -137,7 +161,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (h.item.mission instanceof DownloadMission) {
mPendingDownloadsItems.remove(h);
if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false);
if (mPendingDownloadsItems.size() < 1) {
setAutoRefresh(false);
if (mStartButton != null) mStartButton.setVisible(false);
if (mPauseButton != null) mPauseButton.setVisible(false);
}
}
h.popupMenu.dismiss();
@ -170,10 +198,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
ViewHolderItem h = (ViewHolderItem) view;
h.item = item;
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name);
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName());
h.icon.setImageResource(Utility.getIconForFileType(type));
h.name.setText(item.mission.name);
h.name.setText(item.mission.storage.getName());
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
@ -225,8 +253,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long deltaDone = mission.done - h.lastDone;
boolean hasError = mission.errCode != ERROR_NOTHING;
// on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true
h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength));
// hide on error
// show if current resource length is not fetched
// show if length is unknown
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
float progress;
if (mission.unknownLength) {
@ -252,7 +282,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long length = mission.getLength();
int state;
if (mission.isPsFailed()) {
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
state = 0;
} else if (!mission.running) {
state = mission.enqueued ? 1 : 2;
@ -305,36 +335,78 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
}
private boolean viewWithFileProvider(@NonNull File file) {
if (!file.exists()) return true;
private void viewWithFileProvider(Mission mission) {
if (checkInvalidFile(mission)) return;
String ext = Utility.getFileExt(file.getName());
if (ext == null) return false;
String mimeType = resolveMimeType(mission);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file);
Uri uri;
if (mission.storage.isDirect()) {
uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
new File(URI.create(mission.storage.getUri().toString()))
);
} else {
uri = mission.storage.getUri();
}
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
}
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
}
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.v(TAG, "Starting intent: " + intent);
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
mContext.startActivity(intent);
} else {
Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG);
noPlayerToast.show();
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
}
}
private void shareFile(Mission mission) {
if (checkInvalidFile(mission)) return;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(resolveMimeType(mission));
intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri());
mContext.startActivity(Intent.createChooser(intent, null));
}
private static String resolveMimeType(@NonNull Mission mission) {
String mimeType;
if (!mission.storage.isInvalid()) {
mimeType = mission.storage.getType();
if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME))
return mimeType;
}
String ext = Utility.getFileExt(mission.storage.getName());
if (ext == null) return DEFAULT_MIME_TYPE;
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
}
private boolean checkInvalidFile(@NonNull Mission mission) {
if (mission.storage.existsAsFile()) return false;
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
return true;
}
@ -343,15 +415,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private void onServiceMessage(@NonNull Message msg) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
setAutoRefresh(true);
return;
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
break;
default:
return;
}
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
@ -370,74 +436,104 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private void showError(@NonNull DownloadMission mission) {
StringBuilder str = new StringBuilder();
str.append(mContext.getString(R.string.label_code));
str.append(": ");
str.append(mission.errCode);
str.append('\n');
@StringRes int msg = R.string.general_error;
String msgEx = null;
switch (mission.errCode) {
case 416:
str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable));
msg = R.string.error_http_requested_range_not_satisfiable;
break;
case 404:
str.append(mContext.getString(R.string.error_http_not_found));
msg = R.string.error_http_not_found;
break;
case ERROR_NOTHING:
str.append("¿?");
break;
return;// this never should happen
case ERROR_FILE_CREATION:
str.append(mContext.getString(R.string.error_file_creation));
msg = R.string.error_file_creation;
break;
case ERROR_HTTP_NO_CONTENT:
str.append(mContext.getString(R.string.error_http_no_content));
msg = R.string.error_http_no_content;
break;
case ERROR_HTTP_UNSUPPORTED_RANGE:
str.append(mContext.getString(R.string.error_http_unsupported_range));
msg = R.string.error_http_unsupported_range;
break;
case ERROR_PATH_CREATION:
str.append(mContext.getString(R.string.error_path_creation));
msg = R.string.error_path_creation;
break;
case ERROR_PERMISSION_DENIED:
str.append(mContext.getString(R.string.permission_denied));
msg = R.string.permission_denied;
break;
case ERROR_SSL_EXCEPTION:
str.append(mContext.getString(R.string.error_ssl_exception));
msg = R.string.error_ssl_exception;
break;
case ERROR_UNKNOWN_HOST:
str.append(mContext.getString(R.string.error_unknown_host));
msg = R.string.error_unknown_host;
break;
case ERROR_CONNECT_HOST:
str.append(mContext.getString(R.string.error_connect_host));
msg = R.string.error_connect_host;
break;
case ERROR_POSTPROCESSING_STOPPED:
msg = R.string.error_postprocessing_stopped;
break;
case ERROR_POSTPROCESSING:
str.append(mContext.getString(R.string.error_postprocessing_failed));
case ERROR_POSTPROCESSING_HOLD:
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
return;
case ERROR_INSUFFICIENT_STORAGE:
msg = R.string.error_insufficient_storage;
break;
case ERROR_UNKNOWN_EXCEPTION:
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost;
break;
case ERROR_TIMEOUT:
msg = R.string.error_timeout;
break;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
str = new StringBuilder(8);
str.append("HTTP ");
str.append(mission.errCode);
msgEx = "HTTP " + mission.errCode;
} else if (mission.errObject == null) {
str.append("(not_decelerated_error_code)");
msgEx = "(not_decelerated_error_code)";
} else {
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
return;
}
break;
}
if (mission.errObject != null) {
str.append("\n\n");
str.append(mission.errObject.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
if (msgEx != null)
builder.setMessage(msgEx);
else
builder.setMessage(msg);
// add report button for non-HTTP errors (range 100-599)
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
@StringRes final int mMsg = msg;
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
);
}
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(mission.name)
.setMessage(str)
.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
.setTitle(mission.storage.getName())
.create()
.show();
}
private void showError(Exception exception, UserAction action, @StringRes int reason) {
ErrorActivity.reportError(
mContext,
Collections.singletonList(exception),
null,
null,
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
);
}
public void clearFinishedDownloads() {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
@ -466,16 +562,33 @@ public class MissionAdapter extends Adapter<ViewHolder> {
showError(mission);
return true;
case R.id.queue:
h.queue.setChecked(!h.queue.isChecked());
mission.enqueued = h.queue.isChecked();
boolean flag = !h.queue.isChecked();
h.queue.setChecked(flag);
mission.setEnqueued(flag);
updateProgress(h);
return true;
case R.id.retry:
if (mission.hasInvalidStorage()) {
mDownloadManager.tryRecover(mission);
if (mission.storage.isInvalid())
mRecover.tryRecover(mission);
else
recoverMission(mission);
return true;
}
mission.psContinue(true);
return true;
case R.id.cancel:
mission.psContinue(false);
return false;
}
}
switch (id) {
case R.id.open:
return viewWithFileProvider(h.item.mission.getDownloadedFile());
case R.id.menu_item_share:
shareFile(h.item.mission);
return true;
case R.id.delete:
if (mDeleter == null) {
mDownloadManager.deleteMission(h.item.mission);
@ -486,7 +599,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return true;
case R.id.md5:
case R.id.sha1:
new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id));
new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id));
return true;
case R.id.source:
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
@ -529,29 +642,74 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
public void setClearButton(MenuItem clearButton) {
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
if (mClear == null)
clearButton.setVisible(mIterator.hasFinishedMissions());
mClear = clearButton;
}
public void setMasterButtons(MenuItem startButton, MenuItem pauseButton) {
boolean init = mStartButton == null || mPauseButton == null;
mStartButton = startButton;
mPauseButton = pauseButton;
if (init) checkMasterButtonsVisibility();
}
private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
}
private void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions();
public void deleterDispose(Bundle bundle) {
if (mDeleter != null) mDeleter.dispose(bundle);
mStartButton.setVisible(state[0]);
mPauseButton.setVisible(state[1]);
}
public void deleterLoad(Bundle bundle, View view) {
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(boolean commitChanges) {
if (mDeleter != null) mDeleter.dispose(commitChanges);
}
public void deleterLoad(View view) {
if (mDeleter == null)
mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler);
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
}
public void deleterResume() {
if (mDeleter != null) mDeleter.resume();
}
public void recoverMission(DownloadMission mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue;
mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength()));
h.progress.setMarquee(true);
mDownloadManager.resumeMission(mission);
return;
}
}
private boolean mUpdaterRunning = false;
private final Runnable rUpdater = this::updater;
@ -593,6 +751,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return Float.isNaN(value) || Float.isInfinite(value);
}
public void setRecover(@NonNull RecoverHelper callback) {
mRecover = callback;
}
class ViewHolderItem extends RecyclerView.ViewHolder {
DownloadManager.MissionItem item;
@ -604,6 +766,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
ProgressDrawable progress;
PopupMenu popupMenu;
MenuItem retry;
MenuItem cancel;
MenuItem start;
MenuItem pause;
MenuItem open;
@ -636,22 +800,34 @@ public class MissionAdapter extends Adapter<ViewHolder> {
button.setOnClickListener(v -> showPopupMenu());
Menu menu = popupMenu.getMenu();
retry = menu.findItem(R.id.retry);
cancel = menu.findItem(R.id.cancel);
start = menu.findItem(R.id.start);
pause = menu.findItem(R.id.pause);
open = menu.findItem(R.id.open);
open = menu.findItem(R.id.menu_item_share);
queue = menu.findItem(R.id.queue);
showError = menu.findItem(R.id.error_message_view);
delete = menu.findItem(R.id.delete);
source = menu.findItem(R.id.source);
checksum = menu.findItem(R.id.checksum);
itemView.setOnClickListener((v) -> {
itemView.setHapticFeedbackEnabled(true);
itemView.setOnClickListener(v -> {
if (item.mission instanceof FinishedMission)
viewWithFileProvider(item.mission.getDownloadedFile());
viewWithFileProvider(item.mission);
});
itemView.setOnLongClickListener(v -> {
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
showPopupMenu();
return true;
});
}
private void showPopupMenu() {
retry.setVisible(false);
cancel.setVisible(false);
start.setVisible(false);
pause.setVisible(false);
open.setVisible(false);
@ -664,7 +840,20 @@ public class MissionAdapter extends Adapter<ViewHolder> {
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) {
if (!mission.isPsRunning()) {
if (mission.hasInvalidStorage()) {
retry.setVisible(true);
delete.setVisible(true);
showError.setVisible(true);
} else if (mission.isPsRunning()) {
switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE:
case ERROR_POSTPROCESSING_HOLD:
retry.setVisible(true);
cancel.setVisible(true);
showError.setVisible(true);
break;
}
} else {
if (mission.running) {
pause.setVisible(true);
} else {
@ -713,7 +902,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
static class ChecksumTask extends AsyncTask<String, Void, String> {
static class ChecksumTask extends AsyncTask<Object, Void, String> {
ProgressDialog progressDialog;
WeakReference<Activity> weakReference;
@ -736,8 +925,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
@Override
protected String doInBackground(String... params) {
return Utility.checksum(params[0], params[1]);
protected String doInBackground(Object... params) {
return Utility.checksum((StoredFileHelper) params[0], (String) params[1]);
}
@Override
@ -764,4 +953,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
}
public interface RecoverHelper {
void tryRecover(DownloadMission mission);
}
}

View File

@ -3,8 +3,6 @@ package us.shandian.giga.ui.common;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.view.View;
@ -23,8 +21,6 @@ public class Deleter {
private static final int TIMEOUT = 5000;// ms
private static final int DELAY = 350;// ms
private static final int DELAY_RESUME = 400;// ms
private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names";
private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations";
private Snackbar snackbar;
private ArrayList<Mission> items;
@ -41,7 +37,7 @@ public class Deleter {
private final Runnable rNext;
private final Runnable rCommit;
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
mView = v;
mContext = c;
mAdapter = a;
@ -55,27 +51,6 @@ public class Deleter {
rCommit = this::commit;
items = new ArrayList<>(2);
if (b != null) {
String[] names = b.getStringArray(BUNDLE_NAMES);
String[] locations = b.getStringArray(BUNDLE_LOCATIONS);
if (names == null || locations == null) return;
if (names.length < 1 || locations.length < 1) return;
if (names.length != locations.length) return;
items.ensureCapacity(names.length);
for (int j = 0; j < locations.length; j++) {
Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]);
if (mission == null) continue;
items.add(mission);
mIterator.hide(mission);
}
if (items.size() > 0) resume();
}
}
public void append(Mission item) {
@ -104,7 +79,7 @@ public class Deleter {
private void next() {
if (items.size() < 1) return;
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, s -> forget());
@ -125,7 +100,7 @@ public class Deleter {
mDownloadManager.deleteMission(mission);
if (mission instanceof FinishedMission) {
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
}
break;
}
@ -151,27 +126,14 @@ public class Deleter {
mHandler.postDelayed(rShow, DELAY_RESUME);
}
public void dispose(Bundle bundle) {
public void dispose(boolean commitChanges) {
if (items.size() < 1) return;
pause();
if (bundle == null) {
if (!commitChanges) return;
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
return;
}
String[] names = new String[items.size()];
String[] locations = new String[items.size()];
for (int i = 0; i < items.size(); i++) {
Mission mission = items.get(i);
names[i] = mission.name;
locations[i] = mission.location;
}
bundle.putStringArray(BUNDLE_NAMES, names);
bundle.putStringArray(BUNDLE_LOCATIONS, locations);
}
}

View File

@ -1,7 +1,7 @@
package us.shandian.giga.ui.fragment;
import android.app.Activity;
import android.app.Fragment;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@ -10,6 +10,8 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -18,23 +20,31 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DMBinder;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.ui.adapter.MissionAdapter;
public class MissionsFragment extends Fragment {
private static final int SPAN_SIZE = 2;
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
private SharedPreferences mPrefs;
private boolean mLinear;
private MenuItem mSwitch;
private MenuItem mClear = null;
private MenuItem mStart = null;
private MenuItem mPause = null;
private RecyclerView mList;
private View mEmpty;
@ -43,21 +53,24 @@ public class MissionsFragment extends Fragment {
private LinearLayoutManager mLinearManager;
private Context mContext;
private DMBinder mBinder;
private Bundle mBundle;
private DownloadManagerBinder mBinder;
private boolean mForceUpdate;
private DownloadMission unsafeMissionTarget = null;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mBinder = (DownloadManagerBinder) binder;
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty);
mAdapter.deleterLoad(mBundle, getView());
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(getView());
mBundle = null;
mAdapter.setRecover(MissionsFragment.this::recoverMission);
setAdapterButtons();
mBinder.addMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(false);
@ -74,15 +87,12 @@ public class MissionsFragment extends Fragment {
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false);
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mLinear = mPrefs.getBoolean("linear", false);
//mContext = getActivity().getApplicationContext();
mBundle = savedInstanceState;
// Bind the service
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
@ -132,7 +142,7 @@ public class MissionsFragment extends Fragment {
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity.getApplicationContext();
mContext = activity;
}
@ -144,7 +154,7 @@ public class MissionsFragment extends Fragment {
mBinder.removeMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(true);
mContext.unbindService(mConnection);
mAdapter.deleterDispose(null);
mAdapter.deleterDispose(true);
mBinder = null;
mAdapter = null;
@ -154,7 +164,11 @@ public class MissionsFragment extends Fragment {
public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
if (mAdapter != null) mAdapter.setClearButton(mClear);
mStart = menu.findItem(R.id.start_downloads);
mPause = menu.findItem(R.id.pause_downloads);
if (mAdapter != null) setAdapterButtons();
super.onPrepareOptionsMenu(menu);
}
@ -166,8 +180,23 @@ public class MissionsFragment extends Fragment {
updateList();
return true;
case R.id.clear_list:
mAdapter.clearFinishedDownloads();
AlertDialog.Builder prompt = new AlertDialog.Builder(mContext);
prompt.setTitle(R.string.clear_finished_download);
prompt.setMessage(R.string.confirm_prompt);
prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads());
prompt.setNegativeButton(R.string.cancel, null);
prompt.create().show();
return true;
case R.id.start_downloads:
item.setVisible(false);
mPause.setVisible(true);
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
item.setVisible(false);
mStart.setVisible(true);
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view
default:
return super.onOptionsItemSelected(item);
}
@ -193,9 +222,9 @@ public class MissionsFragment extends Fragment {
int icon;
if (mLinear)
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
else
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
else
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
mSwitch.setIcon(icon);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
@ -203,12 +232,29 @@ public class MissionsFragment extends Fragment {
}
}
private void setAdapterButtons() {
if (mClear == null || mStart == null || mPause == null) return;
mAdapter.setClearButton(mClear);
mAdapter.setMasterButtons(mStart, mPause);
}
private void recoverMission(@NonNull DownloadMission mission) {
unsafeMissionTarget = mission;
StoredFileHelper.requestSafWithFileCreation(
MissionsFragment.this,
REQUEST_DOWNLOAD_PATH_SAF,
mission.storage.getName(),
mission.storage.getType()
);
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
mAdapter.deleterDispose(outState);
mAdapter.deleterDispose(false);
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
}
@ -237,4 +283,23 @@ public class MissionsFragment extends Fragment {
if (mAdapter != null) mAdapter.onPaused();
if (mBinder != null) mBinder.enableNotifications(true);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return;
if (unsafeMissionTarget == null || data.getData() == null) {
return;// unsafeMissionTarget cannot be null
}
try {
String tag = unsafeMissionTarget.storage.getTag();
unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag);
mAdapter.recoverMission(unsafeMissionTarget);
} catch (IOException e) {
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
}
}
}

View File

@ -9,14 +9,15 @@ import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
@ -25,7 +26,8 @@ import java.io.Serializable;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import us.shandian.giga.io.StoredFileHelper;
public class Utility {
@ -80,6 +82,7 @@ public class Utility {
objectInputStream = new ObjectInputStream(new FileInputStream(file));
object = (T) objectInputStream.readObject();
} catch (Exception e) {
Log.e("Utility", "Failed to deserialize the object", e);
object = null;
}
@ -206,7 +209,7 @@ public class Utility {
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}
public static String checksum(String path, String algorithm) {
public static String checksum(StoredFileHelper source, String algorithm) {
MessageDigest md;
try {
@ -215,11 +218,11 @@ public class Utility {
throw new RuntimeException(e);
}
FileInputStream i;
SharpStream i;
try {
i = new FileInputStream(path);
} catch (FileNotFoundException e) {
i = source.getStream();
} catch (Exception e) {
throw new RuntimeException(e);
}
@ -247,15 +250,15 @@ public class Utility {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static boolean mkdir(File path, boolean allDirs) {
if (path.exists()) return true;
public static boolean mkdir(File p, boolean allDirs) {
if (p.exists()) return true;
if (allDirs)
path.mkdirs();
p.mkdirs();
else
path.mkdir();
p.mkdir();
return path.exists();
return p.exists();
}
public static long getContentLength(HttpURLConnection connection) {
@ -264,8 +267,7 @@ public class Utility {
}
try {
long length = Long.parseLong(connection.getHeaderField("Content-Length"));
if (length >= 0) return length;
return Long.parseLong(connection.getHeaderField("Content-Length"));
} catch (Exception err) {
// nothing to do
}

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"
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"
android:visible="false"
android:icon="?attr/ic_delete"

View File

@ -1,4 +1,13 @@
<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
android:id="@+id/start"
android:title="@string/start" />
@ -13,8 +22,8 @@
android:checkable="true"/>
<item
android:id="@+id/open"
android:title="@string/view" />
android:id="@+id/menu_item_share"
android:title="@string/share" />
<item
android:id="@+id/delete"

View File

@ -451,7 +451,7 @@
<string name="app_update_notification_content_title">يتوفر تحديث ل newpipe!</string>
<string name="app_update_notification_content_text">اضغط لتنزيل</string>
<string name="missions_header_finished">انتهى</string>
<string name="missions_header_pending">في قائمة الانتظار</string>
<string name="missions_header_pending">ريثما</string>
<string name="paused">متوقف</string>
<string name="queued">في قائمة الانتظار</string>
<string name="post_processing">قيد المعالجة</string>
@ -462,12 +462,12 @@
<string name="download_finished_more">%s أنتهى التحميل</string>
<string name="generate_unique_name">إنشاء اسم فريد</string>
<string name="overwrite">الكتابة فوق</string>
<string name="overwrite_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
<string name="overwrite_finished_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
<string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string>
<string name="show_error">إظهار خطأ</string>
<string name="label_code">كود</string>
<string name="error_path_creation">لا يمكن إنشاء الملف</string>
<string name="error_file_creation">لا يمكن إنشاء المجلد الوجهة</string>
<string name="error_file_creation">لا يمكن إنشاء الملف</string>
<string name="error_path_creation">لا يمكن إنشاء المجلد الوجهة</string>
<string name="error_permission_denied">تم رفضها من قبل النظام</string>
<string name="error_ssl_exception">فشل اتصال الأمن</string>
<string name="error_unknown_host">تعذر العثور على الخادم</string>

View File

@ -425,15 +425,15 @@
<string name="auto">Automàtic</string>
<string name="switch_view">Canvia la vista</string>
<string name="app_update_notification_content_title">Està disponible una nova actualització del NewPipe!</string>
<string name="missions_header_pending">A la cua</string>
<string name="missions_header_pending">Pendent</string>
<string name="paused">en pausa</string>
<string name="queued">a la cua</string>
<string name="enqueue">Afegeix a la cua</string>
<string name="generate_unique_name">Genera un nom únic</string>
<string name="show_error">Mostra l\'error</string>
<string name="label_code">Codi</string>
<string name="error_path_creation">No es pot crear el fitxer</string>
<string name="error_file_creation">No es pot crear la carpeta de destinació</string>
<string name="error_file_creation">No es pot crear el fitxer</string>
<string name="error_path_creation">No es pot crear la carpeta de destinació</string>
<string name="stop">Atura</string>
<string name="events">Esdeveniments</string>
<string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string>
@ -446,7 +446,8 @@
<string name="download_failed">Ha fallat la baixada</string>
<string name="download_finished">Baixada finalitzada</string>
<string name="download_finished_more">%s baixades finalitzades</string>
<string name="overwrite_warning">Ja existeix un fitxer baixat amb aquest nom</string>
<string name="overwrite_finished_warning">Ja existeix un fitxer baixat amb aquest nom</string>
<string name="overwrite_unrelated_warning">Ja existeix un fitxer amb aquest nom</string>
<string name="download_already_running">Hi ha una baixada en curs amb aquest nom</string>
<string name="error_ssl_exception">Ha fallat la connexió segura</string>
<string name="error_unknown_host">No s\'ha pogut trobar el servidor</string>

View File

@ -426,7 +426,7 @@
<string name="auto">自动</string>
<string name="app_update_notification_content_text">轻按以下载</string>
<string name="missions_header_finished">已完成</string>
<string name="missions_header_pending">于队列中</string>
<string name="missions_header_pending">有待</string>
<string name="paused">已暂停</string>
<string name="queued">已加入队列</string>
<string name="post_processing">后处理</string>
@ -434,14 +434,14 @@
<string name="permission_denied">系统拒绝该行动</string>
<string name="download_failed">下载失败</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="overwrite">覆写</string>
<string name="overwrite_warning">同名的已下载文件已经存在</string>
<string name="overwrite_finished_warning">同名的已下载文件已经存在</string>
<string name="download_already_running">同名下载进行中</string>
<string name="show_error">显示错误</string>
<string name="label_code">代码</string>
<string name="error_path_creation">无法创建该文件</string>
<string name="error_file_creation">无法创建该文件</string>
<string name="error_permission_denied">系统拒绝此批准</string>
<string name="error_ssl_exception">安全连接失败</string>
<string name="error_unknown_host">找不到服务器</string>
@ -464,7 +464,7 @@
<string name="grid">网格</string>
<string name="switch_view">切换视图</string>
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
<string name="error_file_creation">无法创建目标文件夹</string>
<string name="error_path_creation">无法创建目标文件夹</string>
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string>
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>

View File

@ -459,7 +459,8 @@ otevření ve vyskakovacím okně</string>
<string name="download_finished_more">% s stahování dokončeno</string>
<string name="generate_unique_name">Vytvořit jedinečný název</string>
<string name="overwrite">Přepsat</string>
<string name="overwrite_warning">Stažený soubor s tímto názvem již existuje</string>
<string name="overwrite_unrelated_warning">Stažený soubor s tímto názvem již existuje</string>
<string name="overwrite_finished_warning">Stažený soubor s tímto názvem již existuje</string>
<string name="download_already_running">Stahování s tímto názvem již probíhá</string>
<string name="show_error">Zobrazit chybu</string>
<string name="label_code">Kód</string>

View File

@ -359,7 +359,7 @@
<string name="auto">Automatisk</string>
<string name="app_update_notification_content_text">Tryk for at downloade</string>
<string name="missions_header_finished">Færdig</string>
<string name="missions_header_pending">I kø</string>
<string name="missions_header_pending">Afventning</string>
<string name="post_processing">efterbehandling</string>
<string name="enqueue"></string>
<string name="permission_denied">Handling afvist af systemet</string>
@ -368,12 +368,13 @@
<string name="download_finished_more">%s downloads færdige</string>
<string name="generate_unique_name">Generer unikt navn</string>
<string name="overwrite">Overskriv</string>
<string name="overwrite_warning">En downloadet fil med dette navn eksisterer allerede</string>
<string name="overwrite_unrelated_warning">En fil med dette navn eksisterer allerede</string>
<string name="overwrite_finished_warning">En downloadet fil med dette navn eksisterer allerede</string>
<string name="download_already_running">Der er en download i gang med dette navn</string>
<string name="show_error">Vis fejl</string>
<string name="label_code">Kode</string>
<string name="error_path_creation">Filen kan ikke oprettes</string>
<string name="error_file_creation">Destinationsmappen kan ikke oprettes</string>
<string name="error_file_creation">Filen kan ikke oprettes</string>
<string name="error_path_creation">Destinationsmappen kan ikke oprettes</string>
<string name="error_permission_denied">Adgang nægtet af systemet</string>
<string name="error_ssl_exception">Sikker forbindelse fejlede</string>
<string name="error_unknown_host">Kunne ikke finde serveren</string>

View File

@ -437,7 +437,7 @@
<string name="app_update_notification_content_title">NewPipe-Aktualisierung verfügbar!</string>
<string name="app_update_notification_content_text">Zum Herunterladen antippen</string>
<string name="missions_header_finished">Fertig</string>
<string name="missions_header_pending">In der Warteschlange</string>
<string name="missions_header_pending">Ausstehend</string>
<string name="paused">pausiert</string>
<string name="queued">eingereiht</string>
<string name="post_processing">Nachbearbeitung</string>
@ -448,12 +448,12 @@
<string name="download_finished_more">%s heruntergeladen</string>
<string name="generate_unique_name">Eindeutigen Namen erzeugen</string>
<string name="overwrite">Überschreiben</string>
<string name="overwrite_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string>
<string name="download_already_running">Eine Datei dieses Namens wird gerade heruntergeladen</string>
<string name="overwrite_unrelated_warning">Eine Datei mit diesem Namen existiert bereits</string>
<string name="download_already_running">Eine heruntergeladene Datei mit diesem Namen existiert bereits</string>
<string name="show_error">Fehler anzeigen</string>
<string name="label_code">Code</string>
<string name="error_path_creation">Die Datei kann nicht erstellt werden</string>
<string name="error_file_creation">Der Zielordner kann nicht erstellt werden</string>
<string name="error_file_creation">Die Datei kann nicht erstellt werden</string>
<string name="error_path_creation">Der Zielordner kann nicht erstellt werden</string>
<string name="error_permission_denied">System verweigert den Zugriff</string>
<string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string>
<string name="error_unknown_host">Der Server konnte nicht gefunden werden</string>
@ -472,4 +472,5 @@
<string name="pause_downloads_on_mobile_desc">Downloads, die nicht pausiert werden können, werden wiederholt</string>
<string name="conferences">Konferenzen</string>
<string name="events">Ereignisse</string>
<string name="error_timeout">Verbindungszeitüberschreitung</string>
</resources>

View File

@ -444,7 +444,8 @@
<string name="download_finished_more">%s λήψεις ολοκρηρώθηκαν</string>
<string name="generate_unique_name">Δημιουργία μοναδικού ονόματος</string>
<string name="overwrite">Αντικατάσταση</string>
<string name="overwrite_warning">Ένα αρχείο με το ίδιο όνομα υπάρχει ήδη</string>
<string name="overwrite_unrelated_warning">Ένα αρχείο με αυτό το όνομα υπάρχει ήδη</string>
<string name="overwrite_finished_warning">Ένα αρχείο που έχει ληφθεί με αυτό το όνομα υπάρχει ήδη</string>
<string name="download_already_running">Υπάρχει μια λήψη σε εξέλιξη με αυτό το όνομα</string>
<string name="show_error">Εμφάνιση σφάλματος</string>
<string name="label_code">Κωδικός</string>

View File

@ -14,7 +14,7 @@
<string name="share_dialog_title">Compartir con</string>
<string name="choose_browser">Elegir navegador</string>
<string name="screen_rotation">rotación</string>
<string name="download_path_title">Ruta de descarga de vídeo</string>
<string name="download_path_title">Carpeta de descarga de vídeo</string>
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
@ -40,7 +40,7 @@
<string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string>
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
<string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string>
<string name="download_path_audio_summary">Los audios descargados se almacenan aquí</string>
<string name="download_path_audio_summary">Ruta para almacenar los audios descargados</string>
<string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string>
<string name="blocked_by_gema">Bloqueado por GEMA</string>
<string name="download_path_audio_title">Carpeta de descarga de audio</string>
@ -325,6 +325,7 @@ abrir en modo popup</string>
<string name="live">DIRECTO</string>
<string name="live_sync">SINCRONIZAR</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_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>
@ -403,7 +404,7 @@ abrir en modo popup</string>
<string name="playlists">Listas de reproducción</string>
<string name="tracks">Pistas</string>
<string name="missions_header_finished">Finalizadas</string>
<string name="missions_header_pending">En cola</string>
<string name="missions_header_pending">Pendientes</string>
<string name="paused">pausado</string>
<string name="queued">en cola</string>
<string name="post_processing">post-procesado</string>
@ -417,31 +418,51 @@ abrir en modo popup</string>
<!-- dialog about existing downloads -->
<string name="generate_unique_name">Generar nombre único</string>
<string name="overwrite">Sobrescribir</string>
<string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string>
<string name="overwrite_unrelated_warning">Ya existe un archivo con este nombre</string>
<string name="overwrite_finished_warning">Ya existe un archivo descargado con este nombre</string>
<string name="overwrite_failed">No se puede sobrescribir el archivo</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="list">Mostrar como lista</string>
<string name="clear_finished_download">Limpiar descargas finalizadas</string>
<string name="msg_pending_downloads">Continúa tus %s transferencias pendientes desde Descargas</string>
<string name="msg_pending_downloads">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string>
<string name="confirm_prompt">¿Estas seguro?</string>
<string name="stop">Detener</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="pause_downloads_on_mobile">Pausar al cambiar a datos moviles</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">Interrumpir en redes medidas</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 -->
<string name="show_error">Mostrar error</string>
<string name="label_code">Codigo</string>
<string name="error_path_creation">No se puede crear la carpeta de destino</string>
<string name="error_file_creation">No se puede crear el archivo</string>
<string name="error_path_creation">No se puede crear la carpeta de destino</string>
<string name="error_permission_denied">Permiso denegado por el sistema</string>
<string name="error_ssl_exception">Fallo la conexión segura</string>
<string name="error_unknown_host">No se pudo encontrar 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_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_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="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string>
<string name="error_timeout">Tiempo de espera excedido</string>
<string name="download_pick_path">Seleccione los directorios de descarga</string>
<string name="downloads_storage_ask_title">Preguntar dónde descargar</string>
<string name="downloads_storage_ask_summary">Se preguntará dónde guardar cada descarga</string>
<string name="downloads_storage_ask_summary_kitkat">Se preguntará dónde guardar cada descarga.\nHabilita esta opción si quieres descargar en la tarjeta SD externa</string>
<string name="unsubscribe">Desuscribirse</string>
<string name="tab_new">Nueva pestaña</string>
<string name="tab_choose">Elige la pestaña</string>
@ -454,7 +475,7 @@ abrir en modo popup</string>
<string name="app_update_notification_channel_name">Notificación de actualización de la aplicación</string>
<string name="app_update_notification_channel_description">Notificaciones para nueva versión de NewPipe</string>
<string name="download_to_sdcard_error_title">Almacenamiento externo no disponible</string>
<string name="download_to_sdcard_error_message">Todavía no es posible descargar a una tarjeta SD externa. ¿Restablecer la ubicación de la carpeta de descarga\?</string>
<string name="download_to_sdcard_error_message">No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\?</string>
<string name="saved_tabs_invalid_json">Usando las pestañas por defecto, error al leer las pestañas guardadas</string>
<string name="restore_defaults">Restaurar valores por defecto</string>
<string name="restore_defaults_confirmation">¿Quieres restaurar los valores por defecto\?</string>

View File

@ -445,7 +445,8 @@
<string name="download_finished_more">%s allalaadimist lõppenud</string>
<string name="generate_unique_name">Loo kordumatu nimi</string>
<string name="overwrite">Kirjuta üle</string>
<string name="overwrite_warning">Selle nimega allalaetud fail on juba olemas</string>
<string name="overwrite_unrelated_warning">Sellise nimega fail on juba olemas</string>
<string name="overwrite_finished_warning">Selle nimega allalaaditud fail on juba olemas</string>
<string name="download_already_running">Selle nimega allalaadimine on käimas</string>
<string name="show_error">Näita viga</string>
<string name="label_code">Kood</string>

View File

@ -435,7 +435,7 @@
<string name="app_update_notification_content_title">NewPipe eguneraketa eskuragarri!</string>
<string name="app_update_notification_content_text">Sakatu deskargatzeko</string>
<string name="missions_header_finished">Amaituta</string>
<string name="missions_header_pending">Ilaran</string>
<string name="missions_header_pending">Zain</string>
<string name="paused">pausatuta</string>
<string name="queued">ilaran</string>
<string name="post_processing">post-prozesua</string>
@ -446,12 +446,12 @@
<string name="download_finished_more">%s deskarga amaituta</string>
<string name="generate_unique_name">Sortu izen bakana</string>
<string name="overwrite">Gainidatzi</string>
<string name="overwrite_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
<string name="overwrite_finished_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
<string name="download_already_running">Badago izen bera duen deskarga bat abian</string>
<string name="show_error">Erakutsi errorea</string>
<string name="label_code">Kodea</string>
<string name="error_path_creation">Ezin da fitxategia sortu</string>
<string name="error_file_creation">Ezin da helburu karpeta sortu</string>
<string name="error_file_creation">Ezin da fitxategia sortu</string>
<string name="error_path_creation">Ezin da helburu karpeta sortu</string>
<string name="error_permission_denied">Sistemak baimena ukatu du</string>
<string name="error_ssl_exception">Konexio seguruak huts egin du</string>
<string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string>
@ -468,4 +468,5 @@
<string name="max_retry_desc">Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua</string>
<string name="pause_downloads_on_mobile">Pausatu datu mugikorretara aldatzean</string>
<string name="pause_downloads_on_mobile_desc">Pausatu ezin daitezkeen deskargak berrekingo dira</string>
<string name="error_timeout">Konexioaren denbora muga</string>
</resources>

View File

@ -440,13 +440,15 @@
<string name="missions_header_pending">Dans la file d\'attente</string>
<string name="paused">En pause</string>
<string name="download_failed">Téléchargement échoué</string>
<string name="error_timeout">Délai de connection dépassé</string>
<string name="conferences">Conférences</string>
<string name="download_finished">Téléchargement terminé</string>
<string name="download_finished_more">%s téléchargements terminés</string>
<string name="queued">Ajouté à la file d\'attente</string>
<string name="generate_unique_name">Générer un nom unique</string>
<string name="overwrite">Écraser</string>
<string name="overwrite_warning">Un fichier téléchargé avec ce nom existe déjà</string>
<string name="overwrite_unrelated_warning">Un fichier avec ce nom existe déjà</string>
<string name="overwrite_finished_warning">Un fichier téléchargé avec ce nom existe déjà</string>
<string name="download_already_running">Il y a un téléchargement en cours avec ce nom</string>
<string name="show_error">Afficher l\'erreur</string>
<string name="label_code">Code</string>

View File

@ -440,7 +440,6 @@
<string name="app_update_notification_content_title">יצא עדכון ל־NewPipe!</string>
<string name="app_update_notification_content_text">יש לגעת כדי להוריד</string>
<string name="missions_header_finished">הסתיים</string>
<string name="missions_header_pending">בתור</string>
<string name="paused">מושהה</string>
<string name="queued">בתור</string>
<string name="post_processing">עיבוד מאוחר</string>
@ -451,12 +450,12 @@
<string name="download_finished_more">%s הורדות הסתיימו</string>
<string name="generate_unique_name">יצירת שם ייחודי</string>
<string name="overwrite">שכתוב</string>
<string name="overwrite_warning">כבר קיים קובץ בשם הזה</string>
<string name="overwrite_finished_warning">כבר קיים קובץ בשם הזה</string>
<string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string>
<string name="show_error">הצגת שגיאה</string>
<string name="label_code">קוד</string>
<string name="error_path_creation">לא ניתן ליצור את הקובץ</string>
<string name="error_file_creation">לא ניתן ליצור את תיקיית היעד</string>
<string name="error_file_creation">לא ניתן ליצור את הקובץ</string>
<string name="error_path_creation">לא ניתן ליצור את תיקיית היעד</string>
<string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string>
<string name="error_ssl_exception">החיבור המאובטח נכשל</string>
<string name="error_unknown_host">לא ניתן למצוא את השרת</string>
@ -473,4 +472,5 @@
<string name="max_retry_desc">מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה</string>
<string name="pause_downloads_on_mobile">להשהות בעת מעבר לתקשורת נתונים סלולרית</string>
<string name="pause_downloads_on_mobile_desc">הורדות שלא ניתן להשהות יופעלו מחדש</string>
<string name="missions_header_pending">בהמתנה</string>
</resources>

View File

@ -447,7 +447,8 @@
<string name="download_finished_more">%s preuzimanja dovršeno</string>
<string name="generate_unique_name">Generirajte jedinstveni naziv</string>
<string name="overwrite">Piši preko</string>
<string name="overwrite_warning">Preuzeta datoteka s tim nazivom već postoji</string>
<string name="overwrite_unrelated_warning">Datoteka s tim nazivom već postoji</string>
<string name="overwrite_finished_warning">Preuzeta datoteka s tim nazivom već postoji</string>
<string name="download_already_running">U tijeku je preuzimanje s ovim nazivom</string>
<string name="label_code">Kod</string>
<string name="error_path_creation">Datoteku nije moguće izraditi</string>

View File

@ -436,7 +436,7 @@
<string name="app_update_notification_content_title">Pembaruan NewPipe Tersedia!</string>
<string name="app_update_notification_content_text">Ketuk untuk mengunduh</string>
<string name="missions_header_finished">Selesai</string>
<string name="missions_header_pending">Di antrian</string>
<string name="missions_header_pending">Tertunda</string>
<string name="paused">dijeda</string>
<string name="queued">antri</string>
<string name="post_processing">pengolahan-pasca</string>
@ -447,12 +447,12 @@
<string name="download_finished_more">%s unduhan selesai</string>
<string name="generate_unique_name">Hasilkan nama unik</string>
<string name="overwrite">Timpa</string>
<string name="overwrite_warning">File yang diunduh dengan nama ini sudah ada</string>
<string name="overwrite_finished_warning">File yang diunduh dengan nama ini sudah ada</string>
<string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string>
<string name="show_error">Tunjukkan kesalahan</string>
<string name="label_code">Kode</string>
<string name="error_path_creation">File tidak dapat dibuat</string>
<string name="error_file_creation">Folder tujuan tidak dapat dibuat</string>
<string name="error_file_creation">File tidak dapat dibuat</string>
<string name="error_path_creation">Folder tujuan tidak dapat dibuat</string>
<string name="error_permission_denied">Izin ditolak oleh sistem</string>
<string name="error_ssl_exception">Koneksi aman gagal</string>
<string name="error_unknown_host">Tidak dapat menemukan server</string>

View File

@ -438,7 +438,7 @@
<string name="app_update_notification_content_title">Aggiornamento di NewPipe disponibile!</string>
<string name="app_update_notification_content_text">Premi per scaricare</string>
<string name="missions_header_finished">Finito</string>
<string name="missions_header_pending">In coda</string>
<string name="missions_header_pending">In attesa di</string>
<string name="paused">in pausa</string>
<string name="queued">in coda</string>
<string name="post_processing">post-processo</string>
@ -449,12 +449,12 @@
<string name="download_finished_more">%s download finiti</string>
<string name="generate_unique_name">Genera un nome unico</string>
<string name="overwrite">Sovrascrivi</string>
<string name="overwrite_warning">Esiste già un file scaricato con lo stesso nome</string>
<string name="overwrite_finished_warning">Esiste già un file scaricato con lo stesso nome</string>
<string name="download_already_running">C\'è un download in progresso con questo nome</string>
<string name="show_error">Mostra errore</string>
<string name="label_code">Codice</string>
<string name="error_path_creation">Impossibile creare il file</string>
<string name="error_file_creation">Impossibile creare la cartella di destinazione</string>
<string name="error_file_creation">Impossibile creare il file</string>
<string name="error_path_creation">Impossibile creare la cartella di destinazione</string>
<string name="error_permission_denied">Permesso negato dal sistema</string>
<string name="error_ssl_exception">Connessione sicura fallita</string>
<string name="error_unknown_host">Impossibile trovare il server</string>
@ -473,4 +473,5 @@
<string name="pause_downloads_on_mobile_desc">I download che non possono essere messi in pausa verranno riavviati</string>
<string name="events">Eventi</string>
<string name="conferences">Conferenze</string>
<string name="error_timeout">Connesione finita</string>
</resources>

View File

@ -421,7 +421,7 @@
<string name="app_update_notification_content_title">NewPipeのアップデートがあります</string>
<string name="app_update_notification_content_text">タップでダウンロード</string>
<string name="missions_header_finished">終了しました</string>
<string name="missions_header_pending">順番に処理</string>
<string name="missions_header_pending">保留</string>
<string name="paused">一時停止</string>
<string name="queued">順番待ちに追加しました</string>
<string name="post_processing">保存処理をしています</string>
@ -432,7 +432,8 @@
<string name="download_finished_more">%s 件のダウンロード終了</string>
<string name="generate_unique_name">一意の名前を生成します</string>
<string name="overwrite">上書き</string>
<string name="overwrite_warning">同じ名前のファイルが既に存在します</string>
<string name="overwrite_unrelated_warning">この名前のファイルは既に存在します</string>
<string name="overwrite_finished_warning">この名前のダウンロードファイルは既に存在します</string>
<string name="download_already_running">同じ名前を持つダウンロードが既に進行中です</string>
<string name="show_error">エラーを表示する</string>
<string name="label_code">コード</string>
@ -462,6 +463,7 @@
<string name="main_page_content_summary">メインページに表示されるタブ</string>
<string name="updates_setting_description">新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します</string>
<string name="msg_pending_downloads">ダウンロードから %s の保留中の転送を続行します</string>
<string name="pause_downloads_on_mobile">モバイルデータ通信に切替時に、一時停止する</string>
<string name="pause_downloads_on_mobile_desc">一時停止できない場合は再開して継続されます</string>
<string name="pause_downloads_on_mobile">モバイルデータ通信に切り替え時に休止</string>
<string name="pause_downloads_on_mobile_desc">休止できないダウンロードが再開されます</string>
<string name="error_timeout">接続タイムアウト</string>
</resources>

View File

@ -450,7 +450,8 @@
<string name="download_finished_more">%s 다운로드 완료됨</string>
<string name="generate_unique_name">별개의 이름 생성</string>
<string name="overwrite">덮어쓰기</string>
<string name="overwrite_warning">해당 이름을 가진 다운로드된 파일이 이미 존재합니다</string>
<string name="overwrite_unrelated_warning">이 이름을 가진 파일이 이미 있습니다.</string>
<string name="overwrite_finished_warning">이 이름을 가진 다운로드 된 파일이 이미 있습니다.</string>
<string name="download_already_running">해당 이름을 가진 다운로드가 이미 진행중입니다</string>
<string name="show_error">오류 표시</string>
<string name="label_code">코드</string>

View File

@ -432,7 +432,6 @@
<string name="app_update_notification_content_title">Kemas kini NewPipe Tersedia!</string>
<string name="app_update_notification_content_text">Ketik untuk muat turun</string>
<string name="missions_header_finished">Selesai</string>
<string name="missions_header_pending">Dalam barisan</string>
<string name="paused">dijeda</string>
<string name="queued">telah beratur</string>
<string name="post_processing">pemprosesan-pasca</string>
@ -443,12 +442,12 @@
<string name="download_finished_more">%s muat turun selesai</string>
<string name="generate_unique_name">Menjana nama yang unik</string>
<string name="overwrite">Timpa</string>
<string name="overwrite_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
<string name="overwrite_finished_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
<string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string>
<string name="show_error">Tunjukkan kesilapan</string>
<string name="label_code">Kod</string>
<string name="error_path_creation">Fail tidak boleh dibuat</string>
<string name="error_file_creation">Folder destinasi tidak boleh dibuat</string>
<string name="error_file_creation">Fail tidak boleh dibuat</string>
<string name="error_path_creation">Folder destinasi tidak boleh dibuat</string>
<string name="error_permission_denied">Kebenaran ditolak oleh sistem</string>
<string name="error_ssl_exception">Sambungan selamat gagal</string>
<string name="error_unknown_host">Tidak dapat mencari server</string>
@ -465,4 +464,5 @@
<string name="max_retry_desc">Jumlah percubaan maksimum sebelum membatalkan muat turun</string>
<string name="pause_downloads_on_mobile">Jeda semasa beralih ke data mudah alih</string>
<string name="pause_downloads_on_mobile_desc">Muat turun yang tidak dapat dihentikan akan dimulakan semula</string>
<string name="missions_header_pending">Menunggu</string>
</resources>

View File

@ -515,7 +515,6 @@
<string name="app_update_notification_content_title">Ny NewPipe-versjon tilgjengelig.</string>
<string name="app_update_notification_content_text">Trykk for å laste ned</string>
<string name="missions_header_finished">Fullført</string>
<string name="missions_header_pending">I kø</string>
<string name="paused">pauset</string>
<string name="queued">i kø</string>
<string name="post_processing">etterbehandling</string>
@ -526,12 +525,12 @@
<string name="download_finished_more">%s nedlastinger fullført</string>
<string name="generate_unique_name">Generer unikt navn</string>
<string name="overwrite">Overskriv</string>
<string name="overwrite_warning">Nedlastet fil ved dette navnet finnes allerede</string>
<string name="overwrite_finished_warning">Nedlastet fil ved dette navnet finnes allerede</string>
<string name="download_already_running">Nedlasting med dette navnet underveis allerede</string>
<string name="show_error">Vis feil</string>
<string name="label_code">Kode</string>
<string name="error_path_creation">Filen kan ikke opprettes</string>
<string name="error_file_creation">Målmappen kan ikke opprettes</string>
<string name="error_file_creation">Filen kan ikke opprettes</string>
<string name="error_path_creation">Målmappen kan ikke opprettes</string>
<string name="error_permission_denied">Tilgang nektet av systemet</string>
<string name="error_ssl_exception">Sikker tilkobling mislyktes</string>
<string name="error_unknown_host">Fant ikke tjeneren</string>

View File

@ -434,7 +434,6 @@
<string name="app_update_notification_content_title">NewPipe-update beschikbaar!</string>
<string name="app_update_notification_content_text">Tikt voor te downloaden</string>
<string name="missions_header_finished">Voltooid</string>
<string name="missions_header_pending">In wachtrij</string>
<string name="paused">gepauzeerd</string>
<string name="queued">toegevoegd aan wachtrij</string>
<string name="post_processing">nabewerking</string>
@ -445,12 +444,12 @@
<string name="download_finished_more">%s downloads voltooid</string>
<string name="generate_unique_name">Unieke naam genereren</string>
<string name="overwrite">Overschrijven</string>
<string name="overwrite_warning">Der bestaat al een gedownload bestand met deze naam</string>
<string name="overwrite_finished_warning">Der bestaat al een gedownload bestand met deze naam</string>
<string name="download_already_running">Der is al een download met deze naam bezig</string>
<string name="show_error">Foutmelding weergeven</string>
<string name="label_code">Code</string>
<string name="error_path_creation">Het bestand kan niet aangemaakt worden</string>
<string name="error_file_creation">De doelmap kan niet aangemaakt worden</string>
<string name="error_file_creation">Het bestand kan niet aangemaakt worden</string>
<string name="error_path_creation">De doelmap kan niet aangemaakt worden</string>
<string name="error_permission_denied">Toelating geweigerd door het systeem</string>
<string name="error_ssl_exception">Beveiligde verbinding is mislukt</string>
<string name="error_unknown_host">Kon de server niet vinden</string>

View File

@ -438,7 +438,7 @@
<string name="app_update_notification_content_title">NewPipe-update beschikbaar!</string>
<string name="app_update_notification_content_text">Tik om te downloaden</string>
<string name="missions_header_finished">Voltooid</string>
<string name="missions_header_pending">In de wachtrij</string>
<string name="missions_header_pending">In afwachting van</string>
<string name="paused">gepauzeerd</string>
<string name="queued">aan de wachtrij toegevoegd</string>
<string name="post_processing">nabewerking</string>
@ -449,12 +449,12 @@
<string name="download_finished_more">%s downloads voltooid</string>
<string name="generate_unique_name">Genereer een unieke naam</string>
<string name="overwrite">Overschrijven</string>
<string name="overwrite_warning">Er bestaat al een gedownload bestand met deze naam</string>
<string name="overwrite_finished_warning">Er bestaat al een gedownload bestand met deze naam</string>
<string name="download_already_running">Er is een download aan de gang met deze naam</string>
<string name="show_error">Toon foutmelding</string>
<string name="label_code">Code</string>
<string name="error_path_creation">Het bestand kan niet worden gemaakt</string>
<string name="error_file_creation">De doelmap kan niet worden gemaakt</string>
<string name="error_file_creation">Het bestand kan niet worden gemaakt</string>
<string name="error_path_creation">De doelmap kan niet worden gemaakt</string>
<string name="error_permission_denied">Toestemming door het systeem geweigerd</string>
<string name="error_ssl_exception">Beveiligde connectie is mislukt</string>
<string name="error_unknown_host">Kon de server niet vinden</string>
@ -473,4 +473,5 @@
<string name="pause_downloads_on_mobile_desc">Downloads die niet kunnen worden gepauzeerd zullen worden herstart</string>
<string name="events">Gebeurtenissen</string>
<string name="conferences">Conferenties</string>
<string name="error_timeout">Time-out van verbinding</string>
</resources>

View File

@ -435,7 +435,6 @@
<string name="app_update_notification_content_title">Dostępna jest aktualizacja NewPipe!</string>
<string name="app_update_notification_content_text">Stuknij, aby pobrać</string>
<string name="missions_header_finished">Gotowe</string>
<string name="missions_header_pending">W kolejce</string>
<string name="paused">wstrzymane</string>
<string name="queued">w kolejce</string>
<string name="post_processing">przetwarzanie końcowe</string>
@ -446,12 +445,12 @@
<string name="download_finished_more">%s pobieranie zostało zakończone</string>
<string name="generate_unique_name">Wygeneruj unikalną nazwę</string>
<string name="overwrite">Zastąp</string>
<string name="overwrite_warning">Pobrany plik o tej nazwie już istnieje</string>
<string name="overwrite_finished_warning">Pobrany plik o tej nazwie już istnieje</string>
<string name="download_already_running">Trwa pobieranie z tą nazwą</string>
<string name="show_error">Pokaż błąd</string>
<string name="label_code">Kod</string>
<string name="error_path_creation">Nie można utworzyć pliku</string>
<string name="error_file_creation">Nie można utworzyć folderu docelowego</string>
<string name="error_file_creation">Nie można utworzyć pliku</string>
<string name="error_path_creation">Nie można utworzyć folderu docelowego</string>
<string name="error_permission_denied">Odmowa dostępu do systemu</string>
<string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string>
<string name="error_unknown_host">Nie można znaleźć serwera</string>
@ -470,4 +469,5 @@
<string name="pause_downloads_on_mobile_desc">Pobierane pliki, których nie można wstrzymać, zostaną zrestartowane</string>
<string name="events">Zdarzenia</string>
<string name="conferences">Konferencje</string>
<string name="missions_header_pending">Oczekuje</string>
</resources>

View File

@ -435,7 +435,6 @@ abrir em modo popup</string>
<string name="app_update_notification_content_title">Atualização do NewPipe Disponivel!</string>
<string name="app_update_notification_content_text">Toque para baixar</string>
<string name="missions_header_finished">Finalizado</string>
<string name="missions_header_pending">Na fila</string>
<string name="paused">pausado</string>
<string name="queued">adicionado na fila</string>
<string name="post_processing">pós processamento</string>
@ -446,12 +445,12 @@ abrir em modo popup</string>
<string name="download_finished_more">%s downloads terminados</string>
<string name="generate_unique_name">Gerar nome único</string>
<string name="overwrite">"Sobrescrever "</string>
<string name="overwrite_warning">Um arquivo baixado com esse nome já existe</string>
<string name="overwrite_finished_warning">Um arquivo baixado com esse nome já existe</string>
<string name="download_already_running">Existe um download em progresso com esse nome</string>
<string name="show_error">Mostrar erro</string>
<string name="label_code">Código</string>
<string name="error_path_creation">O arquivo não pode ser criado</string>
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
<string name="error_file_creation">O arquivo não pode ser criado</string>
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
<string name="error_permission_denied">Permissão negada pelo sistema</string>
<string name="error_ssl_exception">"Falha na conexão segura "</string>
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
@ -468,4 +467,5 @@ abrir em modo popup</string>
<string name="max_retry_desc">Número máximo de tentativas antes de cancelar o download</string>
<string name="pause_downloads_on_mobile">Pausar quando trocar para dados móveis</string>
<string name="pause_downloads_on_mobile_desc">Downloads que não puderem ser pausados serão reiniciados</string>
<string name="missions_header_pending">Pendente</string>
</resources>

View File

@ -431,7 +431,6 @@
<string name="app_update_notification_content_title">Atualização do NewPipe disponível!</string>
<string name="app_update_notification_content_text">Toque para descarregar</string>
<string name="missions_header_finished">Terminada</string>
<string name="missions_header_pending">Na fila</string>
<string name="paused">em pausa</string>
<string name="queued">na fila</string>
<string name="post_processing">pós-processamento</string>
@ -442,12 +441,12 @@
<string name="download_finished_more">%s descargas terminadas</string>
<string name="generate_unique_name">Gerar nome único</string>
<string name="overwrite">Sobrescrever</string>
<string name="overwrite_warning">Um ficheiro descarregado com este nome já existe</string>
<string name="overwrite_finished_warning">Um ficheiro descarregado com este nome já existe</string>
<string name="download_already_running">Já existe uma descarga em curso com este nome</string>
<string name="show_error">Mostrar erro</string>
<string name="label_code">Código</string>
<string name="error_path_creation">O ficheiro não pode ser criado</string>
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
<string name="error_file_creation">O ficheiro não pode ser criado</string>
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
<string name="error_permission_denied">Permissão negada pelo sistema</string>
<string name="error_ssl_exception">Ligação segura falhou</string>
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
@ -466,4 +465,5 @@
<string name="pause_downloads_on_mobile_desc">Descarregamentos que não podem ser pausados serão reiniciados</string>
<string name="events">Eventos</string>
<string name="conferences">Conferências</string>
<string name="missions_header_pending">Pendente</string>
</resources>

View File

@ -442,12 +442,12 @@
<string name="permission_denied">Действие запрещено системой</string>
<string name="download_failed">Ошибка загрузки</string>
<string name="overwrite">Перезаписать</string>
<string name="overwrite_warning">Файл с таким именем уже существует</string>
<string name="overwrite_finished_warning">Файл с таким именем уже существует</string>
<string name="download_already_running">Загрузка с таким именем уже выполняется</string>
<string name="show_error">Показать текст ошибки</string>
<string name="label_code">Код</string>
<string name="error_path_creation">Файл не может быть создан</string>
<string name="error_file_creation">Папка назначения не может быть создана</string>
<string name="error_path_creation">Папка назначения не может быть создана</string>
<string name="error_file_creation">Файл не может быть создан</string>
<string name="error_permission_denied">Доступ запрещен системой</string>
<string name="error_unknown_host">Сервер не найден</string>
<string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1"</string>
@ -472,4 +472,5 @@
<string name="error_postprocessing_failed">Пост-обработка не удалась</string>
<string name="pause_downloads_on_mobile">Останавливать скачивание при переходе на мобильную сеть</string>
<string name="close">Закрыть</string>
<string name="error_timeout">Время соединения вышло</string>
</resources>

View File

@ -456,7 +456,8 @@
<string name="download_finished_more">%s sťahovania skončené</string>
<string name="generate_unique_name">Vytvoriť jedinečný názov</string>
<string name="overwrite">Prepísať</string>
<string name="overwrite_warning">Stiahnutý súbor s týmto menom už existuje</string>
<string name="overwrite_unrelated_warning">Súbor s týmto názvom už existuje</string>
<string name="overwrite_finished_warning">Stiahnutý súbor s týmto názvom už existuje</string>
<string name="download_already_running">Sťahovanie s týmto názvom už prebieha</string>
<string name="show_error">Zobraziť chybu</string>
<string name="label_code">Kód</string>

View File

@ -48,4 +48,5 @@
<string name="yes">Po</string>
<string name="later">Më vonë</string>
<string name="playback_default">Standard</string>
<string name="missions_header_pending">në pritje të</string>
</resources>

View File

@ -438,7 +438,6 @@
<string name="app_update_notification_content_title">NewPipe Güncellemesi Var!</string>
<string name="app_update_notification_content_text">İndirmek için dokunun</string>
<string name="missions_header_finished">Tamamlandı</string>
<string name="missions_header_pending">Sırada</string>
<string name="paused">durdurulmuş</string>
<string name="queued">sırada</string>
<string name="post_processing">son işlemler uygulanıyor</string>
@ -449,12 +448,12 @@
<string name="download_finished_more">%s indirme bitti</string>
<string name="generate_unique_name">Benzersiz ad oluştur</string>
<string name="overwrite">Üzerine yaz</string>
<string name="overwrite_warning">Bu ada sahip indirilen bir dosya zaten var</string>
<string name="overwrite_finished_warning">Bu ada sahip indirilen bir dosya zaten var</string>
<string name="download_already_running">Bu ad ile devam eden bir indirme var</string>
<string name="show_error">Hatayı göster</string>
<string name="label_code">Kod</string>
<string name="error_path_creation">Dosya oluşturulamıyor</string>
<string name="error_file_creation">Hedef klasör oluşturulamıyor</string>
<string name="error_file_creation">Dosya oluşturulamıyor</string>
<string name="error_path_creation">Hedef klasör oluşturulamıyor</string>
<string name="error_permission_denied">İzin sistem tarafından reddedildi</string>
<string name="error_ssl_exception">Güvenli bağlantı başarısız</string>
<string name="error_unknown_host">Sunucu bulunamadı</string>
@ -473,4 +472,5 @@
<string name="pause_downloads_on_mobile_desc">Duraklatılamayan indirmeler yeniden başlatılacak</string>
<string name="events">Olaylar</string>
<string name="conferences">Konferanslar</string>
<string name="missions_header_pending">Kadar</string>
</resources>

View File

@ -443,7 +443,8 @@
<string name="download_finished_more">%s завантажень завершено</string>
<string name="generate_unique_name">Згенерувати унікальну назву</string>
<string name="overwrite">Перезаписати</string>
<string name="overwrite_warning">Завантажений файл з таким ім\'ям вже існує</string>
<string name="overwrite_unrelated_warning">Файл з такою назвою вже існує</string>
<string name="overwrite_finished_warning">Завантажений файл з такою назвою вже існує</string>
<string name="download_already_running">Файл з такою назвою вже завантажується</string>
<string name="show_error">Показати помилку</string>
<string name="label_code">Код</string>

View File

@ -429,7 +429,6 @@
<string name="app_update_notification_content_title">Đã có bản cập nhật NewPipe!</string>
<string name="app_update_notification_content_text">Nhấn để tải về</string>
<string name="missions_header_finished">Xong</string>
<string name="missions_header_pending">Trong hàng chờ</string>
<string name="paused">đã tạm dừng</string>
<string name="queued">trong hàng đợi</string>
<string name="post_processing">đang xử lý</string>
@ -440,11 +439,11 @@
<string name="download_finished_more">%s tải về đã xong</string>
<string name="generate_unique_name">Tạo tên riêng biệt</string>
<string name="overwrite">Ghi đè</string>
<string name="overwrite_warning">Có một tệp đã tải về trùng tên</string>
<string name="overwrite_finished_warning">Có một tệp đã tải về trùng tên</string>
<string name="download_already_running">Có một tệp trùng tên đang tải về</string>
<string name="show_error">Hiện lỗi</string>
<string name="error_path_creation">Không thể tạo tệp</string>
<string name="error_file_creation">Không thể tạo thư mục đích</string>
<string name="error_file_creation">Không thể tạo tệp</string>
<string name="error_path_creation">Không thể tạo thư mục đích</string>
<string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string>
<string name="error_ssl_exception">Không thể tạo kết nối an toàn</string>
<string name="error_unknown_host">Không thể tìm máy chủ</string>
@ -461,6 +460,7 @@
<string name="max_retry_desc">Số lượt thử lại trước khi hủy tải về</string>
<string name="pause_downloads_on_mobile">Tạm dừng tải khi chuyển qua dữ liệu di động</string>
<string name="pause_downloads_on_mobile_desc">Các tải về không thể tạm dừng được sẽ bắt đầu lại từ đầu</string>
<string name="missions_header_pending">Đang chờ xử lý</string>
<string name="conferences">Hội thảo</string>
<string name="label_code"></string>
</resources>

View File

@ -434,7 +434,7 @@
<string name="app_update_notification_content_title">有可用的 NewPipe 更新!</string>
<string name="app_update_notification_content_text">輕觸以下載</string>
<string name="missions_header_finished">結束</string>
<string name="missions_header_pending">在佇列中</string>
<string name="missions_header_pending">有待</string>
<string name="paused">已暫停</string>
<string name="queued">已排入佇列</string>
<string name="post_processing">正在後處理</string>
@ -445,12 +445,12 @@
<string name="download_finished_more">%s 個下載已結束</string>
<string name="generate_unique_name">生成獨特的名稱</string>
<string name="overwrite">覆寫</string>
<string name="overwrite_warning">已有此名稱的已下載檔案</string>
<string name="overwrite_finished_warning">已有此名稱的已下載檔案</string>
<string name="download_already_running">已有此名稱的當案的下載正在進行</string>
<string name="show_error">顯示錯誤</string>
<string name="label_code">代碼</string>
<string name="error_path_creation">無法建立檔案</string>
<string name="error_file_creation">無法建立目的地資料夾</string>
<string name="error_file_creation">無法建立檔案</string>
<string name="error_path_creation">無法建立目的地資料夾</string>
<string name="error_permission_denied">被系統拒絕的權限</string>
<string name="error_ssl_exception">安全連線失敗</string>
<string name="error_unknown_host">找不到伺服器</string>
@ -469,4 +469,5 @@
<string name="pause_downloads_on_mobile_desc">無法暫停的下載將會重新開始</string>
<string name="events">事件</string>
<string name="conferences">會議</string>
<string name="error_timeout">連接超時</string>
</resources>

View File

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

View File

@ -11,7 +11,7 @@
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
<!-- Key values -->
<string name="download_path_key" translatable="false">download_path</string>
<string name="download_path_video_key" translatable="false">download_path</string>
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
@ -160,14 +160,20 @@
<string name="clear_views_history_key" translatable="false">clear_play_history</string>
<string name="clear_search_history_key" translatable="false">clear_search_history</string>
<string name="downloads_storage_ask" translatable="false">downloads_storage_ask</string>
<!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename</string>
<string name="settings_file_charset_key" translatable="false">file_rename_charset</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
<string name="settings_file_replacement_character_default_value" translatable="false">_</string>
<string name="charset_letters_and_digits_value" translatable="false">CHARSET_LETTERS_AND_DIGITS</string>
<string name="charset_most_special_value" translatable="false">CHARSET_MOST_SPECIAL</string>
<string-array name="settings_filename_charset" translatable="false">
<item>@string/charset_letters_and_digits_value</item>
<item>@string/charset_most_special_characters_value</item>
<item>@string/charset_most_special_value</item>
</string-array>
<string-array name="settings_filename_charset_name" translatable="false">
@ -175,7 +181,7 @@
<item>@string/charset_most_special_characters</item>
</string-array>
<string name="default_file_charset_value" translatable="false">@string/charset_most_special_characters_value</string>
<string name="default_file_charset_value" translatable="false">@string/charset_most_special_value</string>
<string name="downloads_maximum_retry" translatable="false">downloads_max_retry</string>
<string name="downloads_maximum_retry_default" translatable="false">3</string>
@ -192,6 +198,7 @@
</string-array>
<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>

View File

@ -176,7 +176,7 @@
<!-- error strings -->
<string name="general_error">Error</string>
<string name="download_to_sdcard_error_title">External storage unavailable</string>
<string name="download_to_sdcard_error_message">Downloading to external SD card not yet possible. Reset download folder location\?</string>
<string name="download_to_sdcard_error_message">Downloading to external SD card not possible. Reset download folder location\?</string>
<string name="network_error">Network error</string>
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
<string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string>
@ -196,6 +196,7 @@
<string name="invalid_url_toast">Invalid URL</string>
<string name="video_streams_empty">No video 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_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>
@ -304,8 +305,7 @@
<string name="settings_file_charset_title">Allowed characters in filenames</string>
<string name="settings_file_replacement_character_summary">Invalid characters are replaced with this value</string>
<string name="settings_file_replacement_character_title">Replacement character</string>
<string name="charset_letters_and_digits_value" translatable="false">[^\\w\\d]+</string>
<string name="charset_most_special_characters_value" translatable="false">[\\n\\r|\\?*&lt;":&gt;/']+</string>
<string name="charset_letters_and_digits">Letters and digits</string>
<string name="charset_most_special_characters">Most special characters</string>
<string name="toast_no_player">No app installed to play this file</string>
@ -498,7 +498,7 @@
<string name="app_update_notification_content_title">NewPipe Update Available!</string>
<string name="app_update_notification_content_text">Tap to download</string>
<string name="missions_header_finished">Finished</string>
<string name="missions_header_pending">In queue</string>
<string name="missions_header_pending">Pending</string>
<string name="paused">paused</string>
<string name="queued">queued</string>
<string name="post_processing">post-processing</string>
@ -511,13 +511,17 @@
<!-- dialog about existing downloads -->
<string name="generate_unique_name">Generate unique name</string>
<string name="overwrite">Overwrite</string>
<string name="overwrite_warning">A downloaded file with this name already exists</string>
<string name="overwrite_unrelated_warning">A file with this name already exists</string>
<string name="overwrite_finished_warning">A downloaded file with this name already exists</string>
<string name="overwrite_failed">cannot overwrite the file</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 -->
<string name="show_error">Show error</string>
<string name="label_code">Code</string>
<string name="error_path_creation">The file can not be created</string>
<string name="error_file_creation">The destination folder can not be created</string>
<string name="error_file_creation">The file can not be created</string>
<string name="error_path_creation">The destination folder can not be created</string>
<string name="error_permission_denied">Permission denied by the system</string>
<string name="error_ssl_exception">Secure connection failed</string>
<string name="error_unknown_host">Could not find the server</string>
@ -527,13 +531,29 @@
<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_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="error_progress_lost">Progress lost, because the file was deleted</string>
<string name="error_timeout">Connection timeout</string>
<string name="clear_finished_download">Clear finished downloads</string>
<string name="confirm_prompt">Are you sure?</string>
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
<string name="stop">Stop</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="pause_downloads_on_mobile">Pause on switching to mobile data</string>
<string name="pause_downloads_on_mobile_desc">Downloads that can not be paused will be restarted</string>
<string name="pause_downloads_on_mobile">Interrupt on metered networks</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="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>
<string name="download_pick_path">Select the downloads save path</string>
<string name="downloads_storage_ask_title">Ask where to download</string>
<string name="downloads_storage_ask_summary">You will be asked where to save each download</string>
<string name="downloads_storage_ask_summary_kitkat">You will be asked where to save each download.\nEnable this option if you want download to the external SD Card</string>
</resources>

View File

@ -41,6 +41,7 @@
<item name="search_add">@drawable/ic_arrow_top_left_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="pause">@drawable/ic_pause_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_channel">@drawable/ic_channel_black_24dp</item>
@ -119,6 +120,7 @@
<item name="ic_list">@drawable/ic_list_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="pause">@drawable/ic_pause_white_24dp</item>
<item name="ic_settings_update">@drawable/ic_settings_update_white</item>
<item name="separator_color">@color/dark_separator_color</item>

View File

@ -4,10 +4,18 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/settings_category_downloads_title">
<CheckBoxPreference
app:iconSpaceReserved="false"
android:defaultValue="false"
android:key="@string/downloads_storage_ask"
android:summary="@string/downloads_storage_ask_summary_kitkat"
android:title="@string/downloads_storage_ask_title" />
<Preference
app:iconSpaceReserved="false"
android:dialogTitle="@string/download_path_dialog_title"
android:key="@string/download_path_key"
android:key="@string/download_path_video_key"
android:summary="@string/download_path_summary"
android:title="@string/download_path_title"/>
@ -50,4 +58,11 @@
android:summary="@string/pause_downloads_on_mobile_desc"
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>

Binary file not shown.