Merge pull request #1759 from kapodamy/giga-postprocessing
Fucking travis
This commit is contained in:
commit
3599ab3caf
@ -54,7 +54,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:32d316330c26'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:91b1efc97e'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
|
||||
@ -89,7 +89,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
.build();
|
||||
response = client.newCall(request).execute();
|
||||
|
||||
return Long.parseLong(response.header("Content-Length"));
|
||||
String contentLength = response.header("Content-Length");
|
||||
return contentLength == null ? -1 : Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} finally {
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.BaseTransientBottomBar;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class DeleteDownloadManager {
|
||||
|
||||
private static final String KEY_STATE = "delete_manager_state";
|
||||
|
||||
private final View mView;
|
||||
private final HashSet<String> mPendingMap;
|
||||
private final List<Disposable> mDisposableList;
|
||||
private DownloadManager mDownloadManager;
|
||||
private final PublishSubject<DownloadMission> publishSubject = PublishSubject.create();
|
||||
|
||||
DeleteDownloadManager(Activity activity) {
|
||||
mPendingMap = new HashSet<>();
|
||||
mDisposableList = new ArrayList<>();
|
||||
mView = activity.findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
public Observable<DownloadMission> getUndoObservable() {
|
||||
return publishSubject;
|
||||
}
|
||||
|
||||
public boolean contains(@NonNull DownloadMission mission) {
|
||||
return mPendingMap.contains(mission.url);
|
||||
}
|
||||
|
||||
public void add(@NonNull DownloadMission mission) {
|
||||
mPendingMap.add(mission.url);
|
||||
|
||||
if (mPendingMap.size() == 1) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadManager(@NonNull DownloadManager downloadManager) {
|
||||
mDownloadManager = downloadManager;
|
||||
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
|
||||
public void restoreState(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState == null) return;
|
||||
|
||||
List<String> list = savedInstanceState.getStringArrayList(KEY_STATE);
|
||||
if (list != null) {
|
||||
mPendingMap.addAll(list);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveState(@Nullable Bundle outState) {
|
||||
if (outState == null) return;
|
||||
|
||||
for (Disposable disposable : mDisposableList) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap));
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
String url = mPendingMap.iterator().next();
|
||||
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
DownloadMission mission = mDownloadManager.getMission(i);
|
||||
if (url.equals(mission.url)) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) {
|
||||
final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE);
|
||||
final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(l -> snackbar.dismiss());
|
||||
|
||||
mDisposableList.add(disposable);
|
||||
|
||||
snackbar.setAction(R.string.undo, v -> {
|
||||
mPendingMap.remove(mission.url);
|
||||
publishSubject.onNext(mission);
|
||||
disposable.dispose();
|
||||
snackbar.dismiss();
|
||||
});
|
||||
|
||||
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar transientBottomBar, int event) {
|
||||
if (!disposable.isDisposed()) {
|
||||
Completable.fromAction(() -> deletePending(mission))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
}
|
||||
mPendingMap.remove(mission.url);
|
||||
snackbar.removeCallback(this);
|
||||
mDisposableList.remove(disposable);
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
});
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public void deletePending() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
HashSet<Integer> idSet = new HashSet<>();
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (contains(mDownloadManager.getMission(i))) {
|
||||
idSet.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (Integer id : idSet) {
|
||||
mDownloadManager.deleteMission(id);
|
||||
}
|
||||
|
||||
mPendingMap.clear();
|
||||
}
|
||||
|
||||
private void deletePending(@NonNull DownloadMission mission) {
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (mission.url.equals(mDownloadManager.getMission(i).url)) {
|
||||
mDownloadManager.deleteMission(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,16 +15,12 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.AllMissionsFragment;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
|
||||
public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
private DeleteDownloadManager mDeleteDownloadManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@ -47,32 +43,17 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
mDeleteDownloadManager = new DeleteDownloadManager(this);
|
||||
mDeleteDownloadManager.restoreState(savedInstanceState);
|
||||
|
||||
MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG);
|
||||
if (fragment != null) {
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
} else {
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
mDeleteDownloadManager.saveState(outState);
|
||||
super.onSaveInstanceState(outState);
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
MissionsFragment fragment = new AllMissionsFragment();
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
@ -99,7 +80,6 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
deletePending();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
@ -108,14 +88,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
deletePending();
|
||||
}
|
||||
|
||||
private void deletePending() {
|
||||
Completable.fromAction(mDeleteDownloadManager::deletePending)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
public void onRestoreInstanceState(Bundle inState){
|
||||
super.onRestoreInstanceState(inState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -22,38 +26,55 @@ import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
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.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
@State protected StreamInfo currentInfo;
|
||||
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State protected int selectedVideoIndex = 0;
|
||||
@State protected int selectedAudioIndex = 0;
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
@State
|
||||
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected int selectedVideoIndex = 0;
|
||||
@State
|
||||
protected int selectedAudioIndex = 0;
|
||||
@State
|
||||
protected int selectedSubtitleIndex = 0;
|
||||
|
||||
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
|
||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
@ -63,6 +84,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static DownloadDialog newInstance(StreamInfo info) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info);
|
||||
@ -78,6 +101,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
instance.setVideoStreams(streamsList);
|
||||
instance.setSelectedVideoStream(selectedStreamIndex);
|
||||
instance.setAudioStreams(info.getAudioStreams());
|
||||
instance.setSubtitleStreams(info.getSubtitles());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@ -86,7 +111,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
public void setAudioStreams(List<AudioStream> audioStreams) {
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
||||
@ -94,13 +119,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
public void setVideoStreams(List<VideoStream> videoStreams) {
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
||||
this.wrappedVideoStreams = wrappedVideoStreams;
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
|
||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
|
||||
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
|
||||
}
|
||||
|
||||
public void setSelectedVideoStream(int selectedVideoIndex) {
|
||||
this.selectedVideoIndex = selectedVideoIndex;
|
||||
}
|
||||
@ -109,6 +142,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
this.selectedAudioIndex = selectedAudioIndex;
|
||||
}
|
||||
|
||||
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
|
||||
this.selectedSubtitleIndex = selectedSubtitleIndex;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@ -116,7 +153,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
return;
|
||||
@ -125,13 +163,29 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
if (!videoStreams.get(i).isVideoOnly()) continue;
|
||||
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||
|
||||
if (audioStream != null) {
|
||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
||||
} else if (DEBUG) {
|
||||
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
|
||||
}
|
||||
}
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.download_dialog, container);
|
||||
}
|
||||
|
||||
@ -142,6 +196,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
|
||||
@ -154,14 +210,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
||||
int def = 3;
|
||||
threadsCountTextView.setText(String.valueOf(def));
|
||||
threadsSeekBar.setProgress(def - 1);
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
threadsCountTextView.setText(String.valueOf(threads));
|
||||
threadsSeekBar.setProgress(threads - 1);
|
||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
||||
threadsCountTextView.setText(String.valueOf(progress + 1));
|
||||
progress++;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
|
||||
threadsCountTextView.setText(String.valueOf(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -189,6 +249,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -216,7 +281,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
downloadSelected();
|
||||
prepareSelectedDownload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -239,13 +304,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
private void setupSubtitleSpinner() {
|
||||
if (getContext() == null) return;
|
||||
|
||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
|
||||
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
boolean flag = true;
|
||||
|
||||
switch (checkedId) {
|
||||
case R.id.audio_button:
|
||||
setupAudioSpinner();
|
||||
@ -253,7 +329,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
case R.id.video_button:
|
||||
setupVideoSpinner();
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
setupSubtitleSpinner();
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
|
||||
threadsSeekBar.setEnabled(flag);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -262,7 +344,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
@Override
|
||||
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 + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
@ -270,6 +353,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
case R.id.video_button:
|
||||
selectedVideoIndex = position;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedSubtitleIndex = position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,11 +372,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
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 boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
||||
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
@ -298,6 +387,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
} else if (isAudioStreamsAvailable) {
|
||||
audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable) {
|
||||
subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
||||
getDialog().dismiss();
|
||||
@ -307,28 +399,144 @@ 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);
|
||||
}
|
||||
|
||||
private void downloadSelected() {
|
||||
Stream stream;
|
||||
String location;
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
Localization loc = NewPipe.getPreferredLocalization();
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
||||
|
||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||
if (isAudio) {
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
} else {
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
|
||||
if (tag.equalsIgnoreCase(loc.getLanguage())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
String url = stream.getUrl();
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
// fallback
|
||||
// 1st loop match country & language
|
||||
// 2nd loop match language only
|
||||
int index = loc.getLanguage().indexOf("-");
|
||||
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
|
||||
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final Context context = getContext();
|
||||
Stream stream;
|
||||
String location;
|
||||
char kind;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty())
|
||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
||||
kind = 'a';
|
||||
break;
|
||||
case R.id.video_button:
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
||||
kind = 'v';
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together
|
||||
kind = 's';
|
||||
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();
|
||||
}
|
||||
|
||||
final String finalFileName = fileName;
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||
// should be safe run the following code without "getActivity().runOnUiThread()"
|
||||
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();
|
||||
} else {
|
||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
||||
String[] urls;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
if (selectedStream instanceof VideoStream) {
|
||||
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_DASH_MUXER : 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 weak internet connections
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
} else {
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
|
||||
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
||||
getDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
@ -370,14 +371,14 @@ public class VideoDetailFragment
|
||||
Log.w(TAG, "Can't open channel because we got no channel URL");
|
||||
} else {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
@ -744,7 +745,7 @@ public class VideoDetailFragment
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
||||
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
|
||||
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
||||
spinnerToolbar.setAdapter(streamsAdapter);
|
||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@ -1264,6 +1265,7 @@ public class VideoDetailFragment
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
@ -1321,4 +1323,4 @@ public class VideoDetailFragment
|
||||
relatedStreamRootLayout.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,11 +19,11 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
@ -87,7 +87,7 @@ public class PlayerHelper {
|
||||
return pitchFormatter.format(pitch);
|
||||
}
|
||||
|
||||
public static String mimeTypesOf(final SubtitlesFormat format) {
|
||||
public static String subtitleMimeTypesOf(final MediaFormat format) {
|
||||
switch (format) {
|
||||
case VTT: return MimeTypes.TEXT_VTT;
|
||||
case TTML: return MimeTypes.APPLICATION_TTML;
|
||||
@ -97,8 +97,8 @@ public class PlayerHelper {
|
||||
|
||||
@NonNull
|
||||
public static String captionLanguageOf(@NonNull final Context context,
|
||||
@NonNull final Subtitles subtitles) {
|
||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||
@NonNull final SubtitlesStream subtitles) {
|
||||
final String displayName = subtitles.getDisplayLanguageName();
|
||||
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
@ -93,8 +93,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
for (final Subtitles subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
|
||||
103
app/src/main/java/org/schabi/newpipe/streams/DataReader.java
Normal file
103
app/src/main/java/org/schabi/newpipe/streams/DataReader.java
Normal file
@ -0,0 +1,103 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class DataReader {
|
||||
|
||||
public final static int SHORT_SIZE = 2;
|
||||
public final static int LONG_SIZE = 8;
|
||||
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;
|
||||
|
||||
public DataReader(SharpStream stream) {
|
||||
this.rewind = stream.canRewind();
|
||||
this.stream = stream;
|
||||
this.pos = 0L;
|
||||
}
|
||||
|
||||
public long position() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
public final 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();
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public final boolean available() {
|
||||
return stream.available() > 0;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
stream.rewind();
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
public boolean canRewind() {
|
||||
return rewind;
|
||||
}
|
||||
|
||||
private 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;
|
||||
if (read != amount) {
|
||||
throw new EOFException("Truncated data, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
817
app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
Normal file
817
app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
Normal file
File diff suppressed because it is too large
Load Diff
623
app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
Normal file
623
app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,370 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SubtitleConverter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
|
||||
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
|
||||
final FrameWriter callback = new FrameWriter() {
|
||||
int frameIndex = 0;
|
||||
final Charset charset = Charset.forName("utf-8");
|
||||
|
||||
@Override
|
||||
public void yield(SubtitleFrame frame) throws IOException {
|
||||
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
||||
return;
|
||||
}
|
||||
out.write(String.valueOf(frameIndex++).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(getTime(frame.start, true).getBytes(charset));
|
||||
out.write(" --> ".getBytes(charset));
|
||||
out.write(getTime(frame.end, true).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(frame.text.getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
}
|
||||
};
|
||||
|
||||
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
||||
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
||||
new String[]{"timedtext", "head", "wp"},
|
||||
new String[]{"body", "div", "p"},
|
||||
"begin", "end", true
|
||||
);
|
||||
}
|
||||
|
||||
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
||||
String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
|
||||
String timeAttr, String durationAttr, boolean hasTimestamp
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
/*
|
||||
* XML based subtitles parser with BASIC support
|
||||
* multiple CUE is not supported
|
||||
* styling is not supported
|
||||
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
|
||||
* also TimestampTagOption enum is not applicable
|
||||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
byte[] buffer = new byte[source.available()];
|
||||
source.read(buffer);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document xml = builder.parse(new ByteArrayInputStream(buffer));
|
||||
|
||||
String attr;
|
||||
|
||||
// get the format version or namespace
|
||||
Element node = xml.getDocumentElement();
|
||||
|
||||
if (node == null) {
|
||||
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
|
||||
} else if (!node.getNodeName().equals(root)) {
|
||||
throw new ParseException("Invalid root", -1);
|
||||
}
|
||||
|
||||
if (formatAttr.equals("xmlns")) {
|
||||
if (!node.getNamespaceURI().equals(formatVersion)) {
|
||||
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
|
||||
}
|
||||
} else {
|
||||
attr = node.getAttributeNS(formatVersion, formatAttr);
|
||||
if (attr == null) {
|
||||
throw new ParseException("Can't get the format attribute", -1);
|
||||
}
|
||||
if (!attr.equals(formatVersion)) {
|
||||
throw new ParseException("Invalid format version : " + attr, -1);
|
||||
}
|
||||
}
|
||||
|
||||
NodeList node_list;
|
||||
|
||||
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
|
||||
|
||||
if (!hasTimestamp) {
|
||||
node_list = selectNodes(xml, cuePath, formatVersion);
|
||||
|
||||
if (node_list != null) {
|
||||
// if the subtitle has multiple CUEs, use the highest value
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
try {
|
||||
int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
|
||||
if (tmp > line_break) {
|
||||
line_break = tmp;
|
||||
}
|
||||
} catch (Exception err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse every frame
|
||||
node_list = selectNodes(xml, framePath, formatVersion);
|
||||
|
||||
if (node_list == null) {
|
||||
return;// no frames detected
|
||||
}
|
||||
|
||||
int fs_ff = -1;// first timestamp of first frame
|
||||
boolean limit_lines = false;
|
||||
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
Element elem = (Element) node_list.item(i);
|
||||
SubtitleFrame obj = new SubtitleFrame();
|
||||
obj.text = elem.getTextContent();
|
||||
|
||||
attr = elem.getAttribute(timeAttr);// ¡this cant be null!
|
||||
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
|
||||
|
||||
attr = elem.getAttribute(durationAttr);
|
||||
if (obj.text == null || attr == null) {
|
||||
continue;// normally is a blank line (on auto-generated subtitles) ignore
|
||||
}
|
||||
|
||||
if (hasTimestamp) {
|
||||
obj.end = parseTimestamp(attr);
|
||||
|
||||
if (detectYoutubeDuplicateLines) {
|
||||
if (limit_lines) {
|
||||
int swap = obj.end;
|
||||
obj.end = fs_ff;
|
||||
fs_ff = swap;
|
||||
} else {
|
||||
if (fs_ff < 0) {
|
||||
fs_ff = obj.end;
|
||||
} else {
|
||||
if (fs_ff < obj.start) {
|
||||
limit_lines = true;// the subtitles has duplicated lines
|
||||
} else {
|
||||
detectYoutubeDuplicateLines = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj.end = obj.start + Integer.parseInt(attr);
|
||||
}
|
||||
|
||||
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
|
||||
|
||||
// implement auto line breaking (once)
|
||||
StringBuilder text = new StringBuilder(obj.text);
|
||||
obj.text = null;
|
||||
|
||||
switch (text.charAt(line_break)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(line_break, text);
|
||||
break;
|
||||
default:// find the word start position
|
||||
for (int j = line_break - 1; j > 0; j--) {
|
||||
switch (text.charAt(j)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(j, text);
|
||||
j = -1;
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
j = -1;// long word, just ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
obj.text = text.toString();// set the processed text
|
||||
}
|
||||
|
||||
callback.yield(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
|
||||
Element ref = xml.getDocumentElement();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
NodeList nodes = ref.getChildNodes();
|
||||
if (nodes.getLength() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Element elem;
|
||||
for (int j = 0; j < nodes.getLength(); j++) {
|
||||
if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
|
||||
elem = (Element) nodes.item(j);
|
||||
if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
|
||||
ref = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
|
||||
}
|
||||
|
||||
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
|
||||
if (multiImpl.length() < 1) {
|
||||
return 0;
|
||||
} else if (multiImpl.length() == 1) {
|
||||
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
|
||||
}
|
||||
|
||||
// detect wallclock-time
|
||||
if (multiImpl.startsWith("wallclock(")) {
|
||||
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
|
||||
}
|
||||
|
||||
// detect offset-time
|
||||
if (multiImpl.indexOf(':') < 0) {
|
||||
int multiplier = 1000;
|
||||
char metric = multiImpl.charAt(multiImpl.length() - 1);
|
||||
switch (metric) {
|
||||
case 'h':
|
||||
multiplier *= 3600000;
|
||||
break;
|
||||
case 'm':
|
||||
multiplier *= 60000;
|
||||
break;
|
||||
case 's':
|
||||
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
|
||||
multiplier = 1;// ms
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!Character.isDigit(metric)) {
|
||||
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
|
||||
}
|
||||
metric = '\0';
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String offset_time = multiImpl;
|
||||
|
||||
if (multiplier == 1) {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 2);
|
||||
} else if (metric != '\0') {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 1);
|
||||
}
|
||||
|
||||
double time_metric_based = Double.parseDouble(offset_time);
|
||||
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
|
||||
return (int) (time_metric_based * multiplier);
|
||||
}
|
||||
} catch (Exception err) {
|
||||
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
|
||||
}
|
||||
}
|
||||
|
||||
// detect clock-time
|
||||
int time = 0;
|
||||
String[] units = multiImpl.split(":");
|
||||
|
||||
if (units.length < 3) {
|
||||
throw new ParseException("Invalid clock-time timestamp", -1);
|
||||
}
|
||||
|
||||
time += Integer.parseInt(units[0]) * 3600000;// hours
|
||||
time += Integer.parseInt(units[1]) * 60000;//minutes
|
||||
time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
|
||||
|
||||
// frames and sub-frames are ignored (not implemented)
|
||||
// time += units[3] * fps;
|
||||
return time;
|
||||
}
|
||||
|
||||
private static void putBreakAt(int idx, StringBuilder str) {
|
||||
// this should be optimized at compile time
|
||||
|
||||
if (NEW_LINE.length() > 1) {
|
||||
str.delete(idx, idx + 1);// remove after replace
|
||||
str.insert(idx, NEW_LINE);
|
||||
} else {
|
||||
str.setCharAt(idx, NEW_LINE.charAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTime(int time, boolean comma) {
|
||||
// cast every value to integer to avoid auto-round in ToString("00").
|
||||
StringBuilder str = new StringBuilder(12);
|
||||
str.append(numberToString(time / 1000 / 3600, 2));// hours
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 % 60, 2));// seconds
|
||||
str.append(comma ? ',' : '.');
|
||||
str.append(numberToString(time % 1000, 3));// miliseconds
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
private static String numberToString(int nro, int pad) {
|
||||
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* helper classes *
|
||||
******************/
|
||||
|
||||
private interface FrameWriter {
|
||||
|
||||
void yield(SubtitleFrame frame) throws IOException;
|
||||
}
|
||||
|
||||
private static class SubtitleFrame {
|
||||
//Java no support unsigned int
|
||||
|
||||
public int end;
|
||||
public int start;
|
||||
public String text = "";
|
||||
|
||||
private boolean isEmptyText() {
|
||||
if (text == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
switch (text.charAt(i)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\r':
|
||||
case '\n':
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
507
app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
Normal file
507
app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
Normal file
File diff suppressed because it is too large
Load Diff
728
app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
Normal file
728
app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@
|
||||
package org.schabi.newpipe.streams.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* based c#
|
||||
*/
|
||||
public abstract class SharpStream {
|
||||
|
||||
public abstract int read() throws IOException;
|
||||
|
||||
public abstract int read(byte buffer[]) throws IOException;
|
||||
|
||||
public abstract int read(byte buffer[], int offset, int count) throws IOException;
|
||||
|
||||
public abstract long skip(long amount) throws IOException;
|
||||
|
||||
|
||||
public abstract int available();
|
||||
|
||||
public abstract void rewind() throws IOException;
|
||||
|
||||
|
||||
public abstract void dispose();
|
||||
|
||||
public abstract boolean isDisposed();
|
||||
|
||||
|
||||
public abstract boolean canRewind();
|
||||
|
||||
public abstract boolean canRead();
|
||||
|
||||
public abstract boolean canWrite();
|
||||
|
||||
|
||||
public abstract void write(byte value) throws IOException;
|
||||
|
||||
public abstract void write(byte[] buffer) throws IOException;
|
||||
|
||||
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
||||
|
||||
public abstract void flush() throws IOException;
|
||||
|
||||
public void setLength(long length) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SecondaryStreamHelper<T extends Stream> {
|
||||
private final int position;
|
||||
private final StreamSizeWrapper<T> streams;
|
||||
|
||||
public SecondaryStreamHelper(StreamSizeWrapper<T> streams, T selectedStream) {
|
||||
this.streams = streams;
|
||||
this.position = streams.getStreamsList().indexOf(selectedStream);
|
||||
if (this.position < 0) throw new RuntimeException("selected stream not found");
|
||||
}
|
||||
|
||||
public T getStream() {
|
||||
return streams.getStreamsList().get(position);
|
||||
}
|
||||
|
||||
public long getSizeInBytes() {
|
||||
return streams.getSizeInBytes(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* find the correct audio stream for the desired video stream
|
||||
*
|
||||
* @param audioStreams list of audio streams
|
||||
* @param videoStream desired video ONLY stream
|
||||
* @return selected audio stream or null if a candidate was not found
|
||||
*/
|
||||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||
// TODO: check if m4v and m4a selected streams are DASH compliant
|
||||
switch (videoStream.getFormat()) {
|
||||
case WEBM:
|
||||
case MPEG_4:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
|
||||
|
||||
for (AudioStream audio : audioStreams) {
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
// retry, but this time in reverse order
|
||||
for (int i = audioStreams.size() - 1; i >= 0; i--) {
|
||||
AudioStream audio = audioStreams.get(i);
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -13,6 +14,7 @@ import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.Serializable;
|
||||
@ -28,26 +30,34 @@ import us.shandian.giga.util.Utility;
|
||||
/**
|
||||
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
|
||||
*/
|
||||
public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
|
||||
private final Context context;
|
||||
|
||||
private final StreamSizeWrapper<T> streamsWrapper;
|
||||
private final boolean showIconNoAudio;
|
||||
private final SparseArray<SecondaryStreamHelper<U>> secondaryStreams;
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, SparseArray<SecondaryStreamHelper<U>> secondaryStreams) {
|
||||
this.context = context;
|
||||
this.streamsWrapper = streamsWrapper;
|
||||
this.showIconNoAudio = showIconNoAudio;
|
||||
this.secondaryStreams = secondaryStreams;
|
||||
}
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
|
||||
this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null);
|
||||
}
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper) {
|
||||
this(context, streamsWrapper, false);
|
||||
this(context, streamsWrapper, null);
|
||||
}
|
||||
|
||||
public List<T> getAll() {
|
||||
return streamsWrapper.getStreamsList();
|
||||
}
|
||||
|
||||
public SparseArray<SecondaryStreamHelper<U>> getAllSecondary() {
|
||||
return secondaryStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return streamsWrapper.getStreamsList().size();
|
||||
@ -89,29 +99,46 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
String qualityString;
|
||||
|
||||
if (stream instanceof VideoStream) {
|
||||
qualityString = ((VideoStream) stream).getResolution();
|
||||
VideoStream videoStream = ((VideoStream) stream);
|
||||
qualityString = videoStream.getResolution();
|
||||
|
||||
if (!showIconNoAudio) {
|
||||
woSoundIconVisibility = View.GONE;
|
||||
} else if (((VideoStream) stream).isVideoOnly()) {
|
||||
woSoundIconVisibility = View.VISIBLE;
|
||||
} else if (isDropdownItem) {
|
||||
woSoundIconVisibility = View.INVISIBLE;
|
||||
if (secondaryStreams != null) {
|
||||
if (videoStream.isVideoOnly()) {
|
||||
woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE : View.INVISIBLE;
|
||||
} else if (isDropdownItem) {
|
||||
woSoundIconVisibility = View.INVISIBLE;
|
||||
}
|
||||
}
|
||||
} else if (stream instanceof AudioStream) {
|
||||
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
||||
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
||||
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
|
||||
}
|
||||
} else {
|
||||
qualityString = stream.getFormat().getSuffix();
|
||||
}
|
||||
|
||||
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||
SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position);
|
||||
if (secondary != null) {
|
||||
long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
|
||||
sizeView.setText(Utility.formatBytes(size));
|
||||
} else {
|
||||
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||
}
|
||||
sizeView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
sizeView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
if (stream instanceof SubtitlesStream) {
|
||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||
} else {
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
}
|
||||
|
||||
qualityView.setText(qualityString);
|
||||
woSoundIconView.setVisibility(woSoundIconVisibility);
|
||||
|
||||
@ -122,15 +149,17 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
* A wrapper class that includes a way of storing the stream sizes.
|
||||
*/
|
||||
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
||||
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList());
|
||||
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null);
|
||||
private final List<T> streamsList;
|
||||
private final long[] streamSizes;
|
||||
private final String unknownSize;
|
||||
|
||||
public StreamSizeWrapper(List<T> streamsList) {
|
||||
public StreamSizeWrapper(List<T> streamsList, Context context) {
|
||||
this.streamsList = streamsList;
|
||||
this.streamSizes = new long[streamsList.size()];
|
||||
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
|
||||
|
||||
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1;
|
||||
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,7 +172,7 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
final Callable<Boolean> fetchAndSet = () -> {
|
||||
boolean hasChanged = false;
|
||||
for (X stream : streamsWrapper.getStreamsList()) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > 0) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > -2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -173,11 +202,18 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
}
|
||||
|
||||
public String getFormattedSize(int streamIndex) {
|
||||
return Utility.formatBytes(getSizeInBytes(streamIndex));
|
||||
return formatSize(getSizeInBytes(streamIndex));
|
||||
}
|
||||
|
||||
public String getFormattedSize(T stream) {
|
||||
return Utility.formatBytes(getSizeInBytes(stream));
|
||||
return formatSize(getSizeInBytes(stream));
|
||||
}
|
||||
|
||||
private String formatSize(long size) {
|
||||
if (size > -1) {
|
||||
return Utility.formatBytes(size);
|
||||
}
|
||||
return unknownSize;
|
||||
}
|
||||
|
||||
public void setSize(int streamIndex, long sizeInBytes) {
|
||||
@ -193,4 +229,4 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
return (StreamSizeWrapper<X>) EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides access to the storage of {@link DownloadMission}s
|
||||
*/
|
||||
public interface DownloadDataSource {
|
||||
|
||||
/**
|
||||
* Load all missions
|
||||
*
|
||||
* @return a list of download missions
|
||||
*/
|
||||
List<DownloadMission> loadMissions();
|
||||
|
||||
/**
|
||||
* Add a download mission to the storage
|
||||
*
|
||||
* @param downloadMission the download mission to add
|
||||
* @return the identifier of the mission
|
||||
*/
|
||||
void addMission(DownloadMission downloadMission);
|
||||
|
||||
/**
|
||||
* Update a download mission which exists in the storage
|
||||
*
|
||||
* @param downloadMission the download mission to update
|
||||
* @throws IllegalArgumentException if the mission was not added to storage
|
||||
*/
|
||||
void updateMission(DownloadMission downloadMission);
|
||||
|
||||
|
||||
/**
|
||||
* Delete a download mission
|
||||
*
|
||||
* @param downloadMission the mission to delete
|
||||
*/
|
||||
void deleteMission(DownloadMission downloadMission);
|
||||
}
|
||||
186
app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
Normal file
186
app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
Normal file
@ -0,0 +1,186 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
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;
|
||||
|
||||
public class DownloadInitializer extends Thread {
|
||||
private final static String TAG = "DownloadInitializer";
|
||||
final static int mId = 0;
|
||||
|
||||
private DownloadMission mMission;
|
||||
private HttpURLConnection mConn;
|
||||
|
||||
DownloadInitializer(@NonNull DownloadMission mission) {
|
||||
mMission = mission;
|
||||
mConn = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.current > 0) mMission.resetState();
|
||||
|
||||
int retryCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
mMission.currentThreadCount = mMission.threadCount;
|
||||
|
||||
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) {
|
||||
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// check for dynamic generated content
|
||||
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
|
||||
mMission.blocks = 0;
|
||||
mMission.length = 0;
|
||||
mMission.fallback = true;
|
||||
mMission.unknownLength = true;
|
||||
mMission.currentThreadCount = 1;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "falling back (unknown length)");
|
||||
}
|
||||
} else {
|
||||
// Open again
|
||||
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
synchronized (mMission.blockState) {
|
||||
if (mConn.getResponseCode() == 206) {
|
||||
if (mMission.currentThreadCount > 1) {
|
||||
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
|
||||
|
||||
if (mMission.currentThreadCount > mMission.blocks) {
|
||||
mMission.currentThreadCount = (int) mMission.blocks;
|
||||
}
|
||||
if (mMission.currentThreadCount <= 0) {
|
||||
mMission.currentThreadCount = 1;
|
||||
}
|
||||
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
|
||||
mMission.blocks++;
|
||||
}
|
||||
} else {
|
||||
// if one thread is solicited don't calculate blocks, is useless
|
||||
mMission.blocks = 1;
|
||||
mMission.fallback = true;
|
||||
mMission.unknownLength = false;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "http response code = " + mConn.getResponseCode());
|
||||
}
|
||||
} else {
|
||||
// Fallback to single thread
|
||||
mMission.blocks = 0;
|
||||
mMission.fallback = true;
|
||||
mMission.unknownLength = false;
|
||||
mMission.currentThreadCount = 1;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
|
||||
}
|
||||
}
|
||||
|
||||
for (long i = 0; i < mMission.currentThreadCount; i++) {
|
||||
mMission.threadBlockPositions.add(i);
|
||||
mMission.threadBytePositions.add(0L);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
break;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running) return;
|
||||
|
||||
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryCount++ > mMission.maxRetry) {
|
||||
Log.e(TAG, "initializer failed", e);
|
||||
mMission.running = false;
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "initializer failed, retrying", e);
|
||||
}
|
||||
}
|
||||
|
||||
// hide marquee in the progress bar
|
||||
mMission.done++;
|
||||
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
if (mConn != null) {
|
||||
try {
|
||||
mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
public interface DownloadManager {
|
||||
int BLOCK_SIZE = 512 * 1024;
|
||||
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param url the url to download
|
||||
* @param location the location
|
||||
* @param name the name of the file to create
|
||||
* @param isAudio true if the download is an audio file
|
||||
* @param threads the number of threads maximal used to download chunks of the file. @return the identifier of the mission.
|
||||
*/
|
||||
int startMission(String url, String location, String name, boolean isAudio, int threads);
|
||||
|
||||
/**
|
||||
* Resume the execution of a download mission.
|
||||
*
|
||||
* @param id the identifier of the mission to resume.
|
||||
*/
|
||||
void resumeMission(int id);
|
||||
|
||||
/**
|
||||
* Pause the execution of a download mission.
|
||||
*
|
||||
* @param id the identifier of the mission to pause.
|
||||
*/
|
||||
void pauseMission(int id);
|
||||
|
||||
/**
|
||||
* Deletes the mission from the downloaded list but keeps the downloaded file.
|
||||
*
|
||||
* @param id The mission identifier
|
||||
*/
|
||||
void deleteMission(int id);
|
||||
|
||||
/**
|
||||
* Get the download mission by its identifier
|
||||
*
|
||||
* @param id the identifier of the download mission
|
||||
* @return the download mission or null if the mission doesn't exist
|
||||
*/
|
||||
DownloadMission getMission(int id);
|
||||
|
||||
/**
|
||||
* Get the number of download missions.
|
||||
*
|
||||
* @return the number of download missions.
|
||||
*/
|
||||
int getCount();
|
||||
|
||||
}
|
||||
@ -1,395 +0,0 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.download.ExtSDDownloadFailedActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadManagerImpl implements DownloadManager {
|
||||
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
|
||||
private final DownloadDataSource mDownloadDataSource;
|
||||
|
||||
private final ArrayList<DownloadMission> mMissions = new ArrayList<>();
|
||||
@NonNull
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
*
|
||||
* @param searchLocations the directories to search for unfinished downloads
|
||||
* @param downloadDataSource the data source for finished downloads
|
||||
*/
|
||||
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
|
||||
mDownloadDataSource = downloadDataSource;
|
||||
this.context = null;
|
||||
loadMissions(searchLocations);
|
||||
}
|
||||
|
||||
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource, Context context) {
|
||||
mDownloadDataSource = downloadDataSource;
|
||||
this.context = context;
|
||||
loadMissions(searchLocations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int startMission(String url, String location, String name, boolean isAudio, int threads) {
|
||||
DownloadMission existingMission = getMissionByLocation(location, name);
|
||||
if (existingMission != null) {
|
||||
// Already downloaded or downloading
|
||||
if (existingMission.finished) {
|
||||
// Overwrite mission
|
||||
deleteMission(mMissions.indexOf(existingMission));
|
||||
} else {
|
||||
// Rename file (?)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DownloadMission mission = new DownloadMission(name, url, location);
|
||||
mission.timestamp = System.currentTimeMillis();
|
||||
mission.threadCount = threads;
|
||||
mission.addListener(new MissionListener(mission));
|
||||
new Initializer(mission).start();
|
||||
return insertMission(mission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeMission(int i) {
|
||||
DownloadMission d = getMission(i);
|
||||
if (!d.running && d.errCode == -1) {
|
||||
d.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pauseMission(int i) {
|
||||
DownloadMission d = getMission(i);
|
||||
if (d.running) {
|
||||
d.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMission(int i) {
|
||||
DownloadMission mission = getMission(i);
|
||||
if (mission.finished) {
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
}
|
||||
mission.delete();
|
||||
mMissions.remove(i);
|
||||
}
|
||||
|
||||
private void loadMissions(Iterable<String> searchLocations) {
|
||||
mMissions.clear();
|
||||
loadFinishedMissions();
|
||||
for (String location : searchLocations) {
|
||||
loadMissions(location);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a list of mission by its timestamp. Oldest first
|
||||
* @param missions the missions to sort
|
||||
*/
|
||||
static void sortByTimestamp(List<DownloadMission> missions) {
|
||||
Collections.sort(missions, new Comparator<DownloadMission>() {
|
||||
@Override
|
||||
public int compare(DownloadMission o1, DownloadMission o2) {
|
||||
return Long.compare(o1.timestamp, o2.timestamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads finished missions from the data source
|
||||
*/
|
||||
private void loadFinishedMissions() {
|
||||
List<DownloadMission> finishedMissions = mDownloadDataSource.loadMissions();
|
||||
if (finishedMissions == null) {
|
||||
finishedMissions = new ArrayList<>();
|
||||
}
|
||||
// Ensure its sorted
|
||||
sortByTimestamp(finishedMissions);
|
||||
|
||||
mMissions.ensureCapacity(mMissions.size() + finishedMissions.size());
|
||||
for (DownloadMission mission : finishedMissions) {
|
||||
File downloadedFile = mission.getDownloadedFile();
|
||||
if (!downloadedFile.isFile()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "downloaded file removed: " + downloadedFile.getAbsolutePath());
|
||||
}
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
} else {
|
||||
mission.length = downloadedFile.length();
|
||||
mission.finished = true;
|
||||
mission.running = false;
|
||||
mMissions.add(mission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMissions(String location) {
|
||||
|
||||
File f = new File(location);
|
||||
|
||||
if (f.exists() && f.isDirectory()) {
|
||||
File[] subs = f.listFiles();
|
||||
|
||||
if (subs == null) {
|
||||
Log.e(TAG, "listFiles() returned null");
|
||||
return;
|
||||
}
|
||||
|
||||
for (File sub : subs) {
|
||||
if (sub.isFile() && sub.getName().endsWith(".giga")) {
|
||||
DownloadMission mis = Utility.readFromFile(sub.getAbsolutePath());
|
||||
if (mis != null) {
|
||||
if (mis.finished) {
|
||||
if (!sub.delete()) {
|
||||
Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
mis.running = false;
|
||||
mis.recovered = true;
|
||||
insertMission(mis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadMission getMission(int i) {
|
||||
return mMissions.get(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mMissions.size();
|
||||
}
|
||||
|
||||
private int insertMission(DownloadMission mission) {
|
||||
int i = -1;
|
||||
|
||||
DownloadMission m = null;
|
||||
|
||||
if (mMissions.size() > 0) {
|
||||
do {
|
||||
m = mMissions.get(++i);
|
||||
} while (m.timestamp > mission.timestamp && i < mMissions.size() - 1);
|
||||
|
||||
//if (i > 0) i--;
|
||||
} else {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
mMissions.add(i, mission);
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a mission by its location and name
|
||||
*
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @return the mission or null if no such mission exists
|
||||
*/
|
||||
private
|
||||
@Nullable
|
||||
DownloadMission getMissionByLocation(String location, String name) {
|
||||
for (DownloadMission mission : mMissions) {
|
||||
if (location.equals(mission.location) && name.equals(mission.name)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.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;
|
||||
}
|
||||
|
||||
private class Initializer extends Thread {
|
||||
private final DownloadMission mission;
|
||||
private final Handler handler;
|
||||
|
||||
public Initializer(DownloadMission mission) {
|
||||
this.mission = mission;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL url = new URL(mission.url);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
mission.length = conn.getContentLength();
|
||||
|
||||
if (mission.length <= 0) {
|
||||
mission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
|
||||
//mission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open again
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestProperty("Range", "bytes=" + (mission.length - 10) + "-" + mission.length);
|
||||
|
||||
if (conn.getResponseCode() != 206) {
|
||||
// Fallback to single thread if no partial content support
|
||||
mission.fallback = true;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "falling back");
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "response = " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
mission.blocks = mission.length / BLOCK_SIZE;
|
||||
|
||||
if (mission.threadCount > mission.blocks) {
|
||||
mission.threadCount = (int) mission.blocks;
|
||||
}
|
||||
|
||||
if (mission.threadCount <= 0) {
|
||||
mission.threadCount = 1;
|
||||
}
|
||||
|
||||
if (mission.blocks * BLOCK_SIZE < mission.length) {
|
||||
mission.blocks++;
|
||||
}
|
||||
|
||||
|
||||
new File(mission.location).mkdirs();
|
||||
new File(mission.location + "/" + mission.name).createNewFile();
|
||||
RandomAccessFile af = new RandomAccessFile(mission.location + "/" + mission.name, "rw");
|
||||
af.setLength(mission.length);
|
||||
af.close();
|
||||
|
||||
mission.start();
|
||||
} catch (IOException ie) {
|
||||
if(context == null) throw new RuntimeException(ie);
|
||||
|
||||
if(ie.getMessage().contains("Permission denied")) {
|
||||
handler.post(() ->
|
||||
context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class)));
|
||||
} else throw new RuntimeException(ie);
|
||||
} catch (Exception e) {
|
||||
// TODO Notify
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for mission to finish to add it to the {@link #mDownloadDataSource}
|
||||
*/
|
||||
private class MissionListener implements DownloadMission.MissionListener {
|
||||
private final DownloadMission mMission;
|
||||
|
||||
private MissionListener(DownloadMission mission) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
// Could the mission be passed in onFinish()?
|
||||
mMission = mission;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(DownloadMission downloadMission) {
|
||||
mDownloadDataSource.addMission(mMission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(DownloadMission downloadMission, int errCode) {
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,11 @@ package us.shandian.giga.get;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
@ -12,142 +14,166 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
* Runnable to download blocks of a file until the file is completely downloaded,
|
||||
* an error occurs or the process is stopped.
|
||||
*/
|
||||
public class DownloadRunnable implements Runnable {
|
||||
public class DownloadRunnable extends Thread {
|
||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId;
|
||||
|
||||
public DownloadRunnable(DownloadMission mission, int id) {
|
||||
private HttpURLConnection mConn;
|
||||
|
||||
DownloadRunnable(DownloadMission mission, int id) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
mMission = mission;
|
||||
mId = id;
|
||||
mConn = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean retry = mMission.recovered;
|
||||
long position = mMission.getPosition(mId);
|
||||
long blockPosition = mMission.getBlockPosition(mId);
|
||||
int retryCount = 0;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":default pos " + position);
|
||||
Log.d(TAG, mId + ":default pos " + blockPosition);
|
||||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) {
|
||||
RandomAccessFile f;
|
||||
InputStream is = null;
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
mMission.pause();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
} catch (FileNotFoundException e) {
|
||||
mMission.notifyError(e);// this never should happen
|
||||
return;
|
||||
}
|
||||
|
||||
while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING && blockPosition < mMission.blocks) {
|
||||
|
||||
if (DEBUG && retry) {
|
||||
Log.d(TAG, mId + ":retry is true. Resuming at " + position);
|
||||
Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition);
|
||||
}
|
||||
|
||||
// Wait for an unblocked position
|
||||
while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) {
|
||||
while (!retry && blockPosition < mMission.blocks && mMission.isBlockPreserved(blockPosition)) {
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":position " + position + " preserved, passing");
|
||||
Log.d(TAG, mId + ":position " + blockPosition + " preserved, passing");
|
||||
}
|
||||
|
||||
position++;
|
||||
blockPosition++;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
if (position >= mMission.blocks) {
|
||||
if (blockPosition >= mMission.blocks) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":preserving position " + position);
|
||||
Log.d(TAG, mId + ":preserving position " + blockPosition);
|
||||
}
|
||||
|
||||
mMission.preserveBlock(position);
|
||||
mMission.setPosition(mId, position);
|
||||
mMission.preserveBlock(blockPosition);
|
||||
mMission.setBlockPosition(mId, blockPosition);
|
||||
|
||||
long start = position * DownloadManager.BLOCK_SIZE;
|
||||
long end = start + DownloadManager.BLOCK_SIZE - 1;
|
||||
long start = blockPosition * DownloadMission.BLOCK_SIZE;
|
||||
long end = start + DownloadMission.BLOCK_SIZE - 1;
|
||||
long offset = mMission.getThreadBytePosition(mId);
|
||||
|
||||
start += offset;
|
||||
|
||||
if (end >= mMission.length) {
|
||||
end = mMission.length - 1;
|
||||
}
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
|
||||
int total = 0;
|
||||
long total = 0;
|
||||
|
||||
try {
|
||||
URL url = new URL(mMission.url);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
|
||||
mConn = mMission.openConnection(mId, start, end);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":" + conn.getRequestProperty("Range"));
|
||||
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && offset > 0) {
|
||||
retryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
}
|
||||
|
||||
// A server may be ignoring the range request
|
||||
if (conn.getResponseCode() != 206) {
|
||||
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
|
||||
notifyError();
|
||||
// The server may be ignoring the range request
|
||||
if (mConn.getResponseCode() != 206) {
|
||||
mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
|
||||
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode());
|
||||
Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
|
||||
f.seek(start);
|
||||
java.io.InputStream ipt = conn.getInputStream();
|
||||
byte[] buf = new byte[64*1024];
|
||||
f.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
while (start < end && mMission.running) {
|
||||
int len = ipt.read(buf, 0, buf.length);
|
||||
is = mConn.getInputStream();
|
||||
|
||||
if (len == -1) {
|
||||
break;
|
||||
} else {
|
||||
start += len;
|
||||
total += len;
|
||||
f.write(buf, 0, len);
|
||||
notifyProgress(len);
|
||||
}
|
||||
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
||||
int len;
|
||||
|
||||
while (start < end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
||||
f.write(buf, 0, len);
|
||||
start += len;
|
||||
total += len;
|
||||
mMission.notifyProgress(len);
|
||||
}
|
||||
|
||||
if (DEBUG && mMission.running) {
|
||||
Log.d(TAG, mId + ":position " + position + " finished, total length " + total);
|
||||
Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded");
|
||||
}
|
||||
|
||||
f.close();
|
||||
ipt.close();
|
||||
if (mMission.running)
|
||||
mMission.setThreadBytePosition(mId, 0L);// clear byte position for next block
|
||||
else
|
||||
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
|
||||
|
||||
// TODO We should save progress for each thread
|
||||
} catch (Exception e) {
|
||||
// TODO Retry count limit & notify error
|
||||
retry = true;
|
||||
mMission.setThreadBytePosition(mId, total);
|
||||
|
||||
notifyProgress(-total);
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||
|
||||
if (retryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":position " + position + " retrying", e);
|
||||
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
|
||||
}
|
||||
|
||||
retry = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (is != null) is.close();
|
||||
} catch (Exception err) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
try {
|
||||
f.close();
|
||||
} catch (Exception err) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "thread " + mId + " exited main loop");
|
||||
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
||||
}
|
||||
|
||||
if (mMission.errCode == -1 && mMission.running) {
|
||||
if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "no error has happened, notifying");
|
||||
}
|
||||
notifyFinished();
|
||||
mMission.notifyFinished();
|
||||
}
|
||||
|
||||
if (DEBUG && !mMission.running) {
|
||||
@ -155,22 +181,15 @@ public class DownloadRunnable implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyProgress(final long len) {
|
||||
synchronized (mMission) {
|
||||
mMission.notifyProgress(len);
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
try {
|
||||
if (mConn != null) mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyError() {
|
||||
synchronized (mMission) {
|
||||
mMission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
|
||||
mMission.pause();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyFinished() {
|
||||
synchronized (mMission) {
|
||||
mMission.notifyFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,74 +1,139 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
/**
|
||||
* Single-threaded fallback mode
|
||||
*/
|
||||
public class DownloadRunnableFallback extends Thread {
|
||||
private static final String TAG = "DownloadRunnableFallback";
|
||||
|
||||
// Single-threaded fallback mode
|
||||
public class DownloadRunnableFallback implements Runnable {
|
||||
private final DownloadMission mMission;
|
||||
//private int mId;
|
||||
private final int mId = 1;
|
||||
|
||||
public DownloadRunnableFallback(DownloadMission mission) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
//mId = id;
|
||||
private int mRetryCount = 0;
|
||||
private InputStream mIs;
|
||||
private RandomAccessFile mF;
|
||||
private HttpURLConnection mConn;
|
||||
|
||||
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
||||
mMission = mission;
|
||||
mIs = null;
|
||||
mF = null;
|
||||
mConn = null;
|
||||
}
|
||||
|
||||
private void dispose() {
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
} catch (IOException e) {
|
||||
// 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() {
|
||||
try {
|
||||
URL url = new URL(mMission.url);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
boolean done;
|
||||
|
||||
if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) {
|
||||
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
|
||||
} else {
|
||||
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
|
||||
f.seek(0);
|
||||
BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
|
||||
byte[] buf = new byte[512];
|
||||
int len = 0;
|
||||
long start = 0;
|
||||
|
||||
while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) {
|
||||
f.write(buf, 0, len);
|
||||
notifyProgress(len);
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
f.close();
|
||||
ipt.close();
|
||||
if (!mMission.unknownLength) {
|
||||
start = mMission.getThreadBytePosition(0);
|
||||
if (DEBUG && start > 0) {
|
||||
Log.i(TAG, "Resuming a single-thread download at " + start);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||
start = 0;
|
||||
mRetryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
}
|
||||
|
||||
// secondary check for the file length
|
||||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
||||
byte[] buf = new byte[64 * 1024];
|
||||
int len = 0;
|
||||
|
||||
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
|
||||
mF.write(buf, 0, len);
|
||||
start += len;
|
||||
mMission.notifyProgress(len);
|
||||
}
|
||||
|
||||
// if thread goes interrupted check if the last part mIs written. This avoid re-download the whole file
|
||||
done = len == -1;
|
||||
} catch (Exception e) {
|
||||
notifyError(DownloadMission.ERROR_UNKNOWN);
|
||||
dispose();
|
||||
|
||||
// save position
|
||||
mMission.setThreadBytePosition(0, start);
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
|
||||
if (mRetryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
run();// try again
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMission.errCode == -1 && mMission.running) {
|
||||
notifyFinished();
|
||||
}
|
||||
}
|
||||
dispose();
|
||||
|
||||
private void notifyProgress(final long len) {
|
||||
synchronized (mMission) {
|
||||
mMission.notifyProgress(len);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyError(final int err) {
|
||||
synchronized (mMission) {
|
||||
mMission.notifyError(err);
|
||||
mMission.pause();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyFinished() {
|
||||
synchronized (mMission) {
|
||||
if (done) {
|
||||
mMission.notifyFinished();
|
||||
} else {
|
||||
mMission.setThreadBytePosition(0, start);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
if (mConn != null) {
|
||||
try {
|
||||
mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
app/src/main/java/us/shandian/giga/get/FinishedMission.java
Normal file
16
app/src/main/java/us/shandian/giga/get/FinishedMission.java
Normal file
@ -0,0 +1,16 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
timestamp = mission.timestamp;
|
||||
name = mission.name;
|
||||
location = mission.location;
|
||||
kind = mission.kind;
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/us/shandian/giga/get/Mission.java
Normal file
66
app/src/main/java/us/shandian/giga/get/Mission.java
Normal file
@ -0,0 +1,66 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
public abstract class Mission implements Serializable {
|
||||
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
|
||||
|
||||
/**
|
||||
* Source url of the resource
|
||||
*/
|
||||
public String source;
|
||||
|
||||
/**
|
||||
* Length of the current resource
|
||||
*/
|
||||
public long length;
|
||||
|
||||
/**
|
||||
* creation timestamp (and maybe unique identifier)
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public File getDownloadedFile() {
|
||||
return new File(location, name);
|
||||
}
|
||||
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
return getDownloadedFile().delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if this mission is deleted whatever is stored
|
||||
*/
|
||||
public transient boolean deleted = false;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(timestamp);
|
||||
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,19 +7,19 @@ 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 {@link us.shandian.giga.get.DownloadMission}
|
||||
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
|
||||
*/
|
||||
public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
|
||||
|
||||
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 = 2;
|
||||
private static final int DATABASE_VERSION = 3;
|
||||
|
||||
/**
|
||||
* The table name of download missions
|
||||
*/
|
||||
@ -30,9 +30,9 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
*/
|
||||
static final String KEY_LOCATION = "location";
|
||||
/**
|
||||
* The key to the url of a mission
|
||||
* The key to the urls of a mission
|
||||
*/
|
||||
static final String KEY_URL = "url";
|
||||
static final String KEY_SOURCE_URL = "url";
|
||||
/**
|
||||
* The key to the name of a mission
|
||||
*/
|
||||
@ -45,6 +45,8 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
|
||||
static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
static final String KEY_KIND = "kind";
|
||||
|
||||
/**
|
||||
* The statement to create the table
|
||||
*/
|
||||
@ -52,16 +54,28 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
|
||||
KEY_LOCATION + " TEXT NOT NULL, " +
|
||||
KEY_NAME + " TEXT NOT NULL, " +
|
||||
KEY_URL + " 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 + "));";
|
||||
|
||||
|
||||
DownloadMissionSQLiteHelper(Context context) {
|
||||
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.
|
||||
*
|
||||
@ -70,34 +84,29 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
*/
|
||||
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_URL, downloadMission.url);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// Currently nothing to do
|
||||
}
|
||||
|
||||
public static DownloadMission getMissionFromCursor(Cursor cursor) {
|
||||
public static FinishedMission getMissionFromCursor(Cursor cursor) {
|
||||
if (cursor == null) throw new NullPointerException("cursor is null");
|
||||
int pos;
|
||||
String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
|
||||
String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
|
||||
String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL));
|
||||
DownloadMission mission = new DownloadMission(name, url, location);
|
||||
mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||
|
||||
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.finished = true;
|
||||
mission.kind = kind.charAt(0);
|
||||
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
@ -1,79 +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.DownloadDataSource;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME;
|
||||
|
||||
|
||||
/**
|
||||
* Non-thread-safe implementation of {@link DownloadDataSource}
|
||||
*/
|
||||
public class SQLiteDownloadDataSource implements DownloadDataSource {
|
||||
|
||||
private static final String TAG = "DownloadDataSourceImpl";
|
||||
private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper;
|
||||
|
||||
public SQLiteDownloadDataSource(Context context) {
|
||||
downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DownloadMission> loadMissions() {
|
||||
ArrayList<DownloadMission> result;
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
|
||||
null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP);
|
||||
|
||||
int count = cursor.getCount();
|
||||
if (count == 0) return new ArrayList<>();
|
||||
result = new ArrayList<>(count);
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
|
||||
database.insert(MISSIONS_TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionSQLiteHelper.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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
database.delete(MISSIONS_TABLE_NAME,
|
||||
KEY_LOCATION + " = ? AND " +
|
||||
KEY_NAME + " = ?",
|
||||
new String[]{downloadMission.location, downloadMission.name});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import org.schabi.newpipe.streams.Mp4DashWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class Mp4DashMuxer extends Postprocessing {
|
||||
|
||||
Mp4DashMuxer(DownloadMission mission) {
|
||||
super(mission);
|
||||
recommendedReserve = 15360 * 1024;// 15 MiB
|
||||
worksOnSameFile = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
Mp4DashWriter muxer = new Mp4DashWriter(sources);
|
||||
muxer.parseSources();
|
||||
muxer.selectTracks(0, 0);
|
||||
muxer.build(out);
|
||||
|
||||
return OK_RESULT;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.postprocessing.io.CircularFile;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
public abstract class Postprocessing {
|
||||
|
||||
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
|
||||
|
||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
|
||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
private static final String ALGORITHM_TEST_ALGO = "test";
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
||||
if (null == algorithmName) {
|
||||
throw new NullPointerException("algorithmName");
|
||||
} else switch (algorithmName) {
|
||||
case ALGORITHM_TTML_CONVERTER:
|
||||
return new TttmlConverter(mission);
|
||||
case ALGORITHM_MP4_DASH_MUXER:
|
||||
return new Mp4DashMuxer(mission);
|
||||
case ALGORITHM_WEBM_MUXER:
|
||||
return new WebMMuxer(mission);
|
||||
case ALGORITHM_TEST_ALGO:
|
||||
return new TestAlgo(mission);
|
||||
/*case "example-algorithm":
|
||||
return new ExampleAlgorithm(mission);*/
|
||||
default:
|
||||
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a boolean value that indicate if the given algorithm work on the same
|
||||
* file
|
||||
*/
|
||||
public boolean worksOnSameFile;
|
||||
|
||||
/**
|
||||
* Get the recommended space to reserve for the given algorithm. The amount
|
||||
* is in bytes
|
||||
*/
|
||||
public int recommendedReserve;
|
||||
|
||||
protected DownloadMission mission;
|
||||
|
||||
Postprocessing(DownloadMission mission) {
|
||||
this.mission = mission;
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
File file = mission.getDownloadedFile();
|
||||
CircularFile out = null;
|
||||
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(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
||||
|
||||
int[] idx = {0};
|
||||
CircularFile.OffsetChecker checker = () -> {
|
||||
while (idx[0] < sources.length) {
|
||||
/*
|
||||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||
* or the CircularFile can lead to unexpected results
|
||||
*/
|
||||
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
|
||||
idx[0]++;
|
||||
continue;// the selected source is not used anymore
|
||||
}
|
||||
|
||||
return sources[idx[0]].getFilePointer() - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
out = new CircularFile(file, 0, this::progressReport, checker);
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = file.length();
|
||||
|
||||
int result = process(out, sources);
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
long finalLength = out.finalizeFile();
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
} else {
|
||||
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
if (result != OK_RESULT && worksOnSameFile) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
new File(mission.location, mission.name).delete();
|
||||
}
|
||||
} finally {
|
||||
for (SharpStream source : sources) {
|
||||
if (source != null && !source.isDisposed()) {
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to execute the pos-processing algorithm
|
||||
*
|
||||
* @param out output stream
|
||||
* @param sources files to be processed
|
||||
* @return a error code, 0 means the operation was successful
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
|
||||
String getArgumentAt(int index, String defaultValue) {
|
||||
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return mission.postprocessingArgs[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
Message m = new Message();
|
||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||
m.obj = mission;
|
||||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* Algorithm for testing proposes
|
||||
*/
|
||||
class TestAlgo extends Postprocessing {
|
||||
|
||||
public TestAlgo(DownloadMission mission) {
|
||||
super(mission);
|
||||
|
||||
worksOnSameFile = true;
|
||||
recommendedReserve = 4096 * 1024;// 4 KiB
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
|
||||
int written = 0;
|
||||
int size = 5 * 1024 * 1024;// 5 MiB
|
||||
byte[] buffer = new byte[8 * 1024];//8 KiB
|
||||
mission.length = size;
|
||||
|
||||
Random rnd = new Random();
|
||||
|
||||
// only write random data
|
||||
sources[0].dispose();
|
||||
|
||||
while (written < size) {
|
||||
rnd.nextBytes(buffer);
|
||||
|
||||
int read = Math.min(buffer.length, size - written);
|
||||
out.write(buffer, 0, read);
|
||||
|
||||
try {
|
||||
Thread.sleep((int) (Math.random() * 10));
|
||||
} catch (InterruptedException e) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
written += read;
|
||||
}
|
||||
|
||||
return Postprocessing.OK_RESULT;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.schabi.newpipe.streams.SubtitleConverter;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
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 TttmlConverter extends Postprocessing {
|
||||
private static final String TAG = "TttmlConverter";
|
||||
|
||||
TttmlConverter(DownloadMission mission) {
|
||||
super(mission);
|
||||
recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||
worksOnSameFile = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
// check if the subtitle is already in srt and copy, this should never happen
|
||||
String format = getArgumentAt(0, null);
|
||||
|
||||
if (format == null || format.equals("ttml")) {
|
||||
SubtitleConverter ttmlDumper = new SubtitleConverter();
|
||||
|
||||
try {
|
||||
ttmlDumper.dumpTTML(
|
||||
sources[0],
|
||||
out,
|
||||
getArgumentAt(1, "true").equals("true"),
|
||||
getArgumentAt(2, "true").equals("true")
|
||||
);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "subtitle parse failed", err);
|
||||
|
||||
if (err instanceof IOException) {
|
||||
return 1;
|
||||
} else if (err instanceof ParseException) {
|
||||
return 2;
|
||||
} else if (err instanceof SAXException) {
|
||||
return 3;
|
||||
} else if (err instanceof ParserConfigurationException) {
|
||||
return 4;
|
||||
} else if (err instanceof XPathExpressionException) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
return 8;
|
||||
}
|
||||
|
||||
return OK_RESULT;
|
||||
} else if (format.equals("srt")) {
|
||||
byte[] buffer = new byte[8 * 1024];
|
||||
int read;
|
||||
while ((read = sources[0].read(buffer)) > 0) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
return OK_RESULT;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.TrackKind;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.WebMWriter;
|
||||
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);
|
||||
recommendedReserve = 2048 * 1024;// 2 MiB
|
||||
worksOnSameFile = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
WebMWriter muxer = new WebMWriter(sources);
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
muxer.selectTracks(0, audioTrackIndex);
|
||||
muxer.build(out);
|
||||
|
||||
return OK_RESULT;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
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;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
|
||||
private RandomAccessFile 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);
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
throw new IOException("The chunk is empty or invalid");
|
||||
}
|
||||
if (source.length() < end) {
|
||||
try {
|
||||
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
||||
} finally {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
source.seek(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute position on file
|
||||
*
|
||||
* @return the position
|
||||
*/
|
||||
public long getFilePointer() {
|
||||
return offset + position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if ((position + 1) > length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int res = source.read();
|
||||
if (res >= 0) {
|
||||
position++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long pos) throws IOException {
|
||||
pos = Math.min(pos + position, length);
|
||||
|
||||
if (pos == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
source.seek(offset + pos);
|
||||
|
||||
long oldPos = position;
|
||||
position = pos;
|
||||
|
||||
return pos - oldPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return (int) (length - position);
|
||||
}
|
||||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public void dispose() {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException err) {
|
||||
} finally {
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
return source == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
position = 0;
|
||||
source.seek(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,375 @@
|
||||
package us.shandian.giga.postprocessing.io;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class CircularFile extends SharpStream {
|
||||
|
||||
private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB
|
||||
private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB
|
||||
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
|
||||
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
|
||||
private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false;
|
||||
|
||||
private RandomAccessFile out;
|
||||
private long position;
|
||||
private long maxLengthKnown = -1;
|
||||
|
||||
private ArrayList<ManagedBuffer> auxiliaryBuffers;
|
||||
private OffsetChecker callback;
|
||||
private ManagedBuffer queue;
|
||||
private long startOffset;
|
||||
private ProgressReport onProgress;
|
||||
private long reportPosition;
|
||||
|
||||
public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException {
|
||||
if (checker == null) {
|
||||
throw new NullPointerException("checker is null");
|
||||
}
|
||||
|
||||
try {
|
||||
queue = new ManagedBuffer(QUEUE_BUFFER_SIZE);
|
||||
out = new RandomAccessFile(file, "rw");
|
||||
out.seek(offset);
|
||||
position = offset;
|
||||
} catch (IOException err) {
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
auxiliaryBuffers = new ArrayList<>(15);
|
||||
callback = checker;
|
||||
startOffset = offset;
|
||||
reportPosition = offset;
|
||||
onProgress = progressReport;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the file without flushing any buffer
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
try {
|
||||
auxiliaryBuffers = null;
|
||||
if (out != null) {
|
||||
out.close();
|
||||
out = null;
|
||||
}
|
||||
} catch (IOException err) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any buffer and close the output file. Use this method if the
|
||||
* operation is successful
|
||||
*
|
||||
* @return the final length of the file
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public long finalizeFile() throws IOException {
|
||||
flushEverything();
|
||||
|
||||
if (maxLengthKnown > -1) {
|
||||
position = maxLengthKnown;
|
||||
}
|
||||
if (position < out.length()) {
|
||||
out.setLength(position);
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b) throws IOException {
|
||||
write(new byte[]{b}, 0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long end = callback.check();
|
||||
long available;
|
||||
|
||||
if (end == -1) {
|
||||
available = Long.MAX_VALUE;
|
||||
} else {
|
||||
if (end < startOffset) {
|
||||
throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end));
|
||||
}
|
||||
available = end - position;
|
||||
}
|
||||
|
||||
// Check if possible flush one or more auxiliary buffer
|
||||
if (auxiliaryBuffers.size() > 0) {
|
||||
ManagedBuffer aux = auxiliaryBuffers.get(0);
|
||||
|
||||
// check if there is enough space to flush it completely
|
||||
while (available >= (aux.size + queue.size)) {
|
||||
available -= aux.size;
|
||||
writeQueue(aux.buffer, 0, aux.size);
|
||||
aux.dereference();
|
||||
auxiliaryBuffers.remove(0);
|
||||
|
||||
if (auxiliaryBuffers.size() < 1) {
|
||||
aux = null;
|
||||
break;
|
||||
}
|
||||
aux = auxiliaryBuffers.get(0);
|
||||
}
|
||||
|
||||
if (IMMEDIATE_AUX_BUFFER_FLUSH) {
|
||||
// try partial flush to avoid allocate another auxiliary buffer
|
||||
if (aux != null && aux.available() < len && available > queue.size) {
|
||||
int size = Math.min(aux.size, (int) available - queue.size);
|
||||
|
||||
writeQueue(aux.buffer, 0, size);
|
||||
aux.dereference(size);
|
||||
|
||||
available -= size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) {
|
||||
writeQueue(b, off, len);
|
||||
} else {
|
||||
int i = auxiliaryBuffers.size() - 1;
|
||||
while (len > 0) {
|
||||
if (i < 0) {
|
||||
// allocate a new auxiliary buffer
|
||||
auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE));
|
||||
i++;
|
||||
}
|
||||
|
||||
ManagedBuffer aux = auxiliaryBuffers.get(i);
|
||||
available = aux.available();
|
||||
|
||||
if (available < 1) {
|
||||
// secondary auxiliary buffer
|
||||
available = len;
|
||||
aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2));
|
||||
auxiliaryBuffers.add(aux);
|
||||
i++;
|
||||
} else {
|
||||
available = Math.min(len, available);
|
||||
}
|
||||
|
||||
aux.write(b, off, (int) available);
|
||||
|
||||
len -= available;
|
||||
if (len > 0) off += available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeOutside(byte buffer[], int offset, int length) throws IOException {
|
||||
out.write(buffer, offset, length);
|
||||
position += length;
|
||||
|
||||
if (onProgress != null && position > reportPosition) {
|
||||
reportPosition = position + NOTIFY_BYTES_INTERVAL;
|
||||
onProgress.report(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeQueue(byte[] buffer, int offset, int length) throws IOException {
|
||||
while (length > 0) {
|
||||
if (queue.available() < length) {
|
||||
flushQueue();
|
||||
|
||||
if (length >= queue.buffer.length) {
|
||||
writeOutside(buffer, offset, length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int size = Math.min(queue.available(), length);
|
||||
queue.write(buffer, offset, size);
|
||||
|
||||
offset += size;
|
||||
length -= size;
|
||||
}
|
||||
|
||||
if (queue.size >= queue.buffer.length) {
|
||||
flushQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private void flushQueue() throws IOException {
|
||||
writeOutside(queue.buffer, 0, queue.size);
|
||||
queue.size = 0;
|
||||
}
|
||||
|
||||
private void flushEverything() throws IOException {
|
||||
flushQueue();
|
||||
|
||||
if (auxiliaryBuffers.size() > 0) {
|
||||
for (ManagedBuffer aux : auxiliaryBuffers) {
|
||||
writeOutside(aux.buffer, 0, aux.size);
|
||||
aux.dereference();
|
||||
}
|
||||
auxiliaryBuffers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any buffer directly to the file. Warning: use this method ONLY if
|
||||
* all read dependencies are disposed
|
||||
*
|
||||
* @throws IOException if the dependencies are not disposed
|
||||
*/
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (callback.check() != -1) {
|
||||
throw new IOException("All read dependencies of this file must be disposed first");
|
||||
}
|
||||
flushEverything();
|
||||
|
||||
// Save the current file length in case the method {@code rewind()} is called
|
||||
if (position > maxLengthKnown) {
|
||||
maxLengthKnown = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
flush();
|
||||
out.seek(startOffset);
|
||||
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-position);
|
||||
}
|
||||
|
||||
position = startOffset;
|
||||
reportPosition = startOffset;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
flush();
|
||||
position += amount;
|
||||
|
||||
out.seek(position);
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
return out == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="stub read methods">
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
throw new UnsupportedOperationException("write-only");
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
public interface OffsetChecker {
|
||||
|
||||
/**
|
||||
* Checks the amount of available space ahead
|
||||
*
|
||||
* @return absolute offset in the file where no more data SHOULD NOT be
|
||||
* written. If the value is -1 the whole file will be used
|
||||
*/
|
||||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
class ManagedBuffer {
|
||||
|
||||
byte[] buffer;
|
||||
int size;
|
||||
|
||||
ManagedBuffer(int length) {
|
||||
buffer = new byte[length];
|
||||
}
|
||||
|
||||
void dereference() {
|
||||
buffer = null;
|
||||
size = 0;
|
||||
}
|
||||
|
||||
void dereference(int amount) {
|
||||
if (amount > size) {
|
||||
throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")");
|
||||
}
|
||||
size -= amount;
|
||||
System.arraycopy(buffer, amount, buffer, 0, size);
|
||||
}
|
||||
|
||||
protected int available() {
|
||||
return buffer.length - size;
|
||||
}
|
||||
|
||||
private void write(byte[] b, int off, int len) {
|
||||
System.arraycopy(b, off, buffer, size, len);
|
||||
size += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package us.shandian.giga.postprocessing.io;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class FileStream extends SharpStream {
|
||||
|
||||
public enum Mode {
|
||||
Read,
|
||||
ReadWrite
|
||||
}
|
||||
|
||||
public RandomAccessFile source;
|
||||
private final Mode mode;
|
||||
|
||||
public FileStream(String path, Mode mode) throws IOException {
|
||||
String flags;
|
||||
|
||||
if (mode == Mode.Read) {
|
||||
flags = "r";
|
||||
} else {
|
||||
flags = "rw";
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
source = new RandomAccessFile(path, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return source.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
return source.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long pos) throws IOException {
|
||||
FileChannel fc = source.getChannel();
|
||||
fc.position(fc.position() + pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
try {
|
||||
return (int) (source.length() - source.getFilePointer());
|
||||
} catch (IOException ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public void dispose() {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException err) {
|
||||
|
||||
} finally {
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
return source == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind() throws IOException {
|
||||
source.getChannel().position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRewind() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead() {
|
||||
return mode == Mode.Read || mode == Mode.ReadWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite() {
|
||||
return mode == Mode.ReadWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte value) throws IOException {
|
||||
source.write(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
source.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||
source.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLength(long length) throws IOException {
|
||||
source.setLength(length);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package us.shandian.giga.postprocessing.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Wrapper for the classic {@link java.io.InputStream}
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SharpInputStream extends InputStream {
|
||||
|
||||
private final SharpStream base;
|
||||
|
||||
public SharpInputStream(SharpStream base) throws IOException {
|
||||
if (!base.canRead()) {
|
||||
throw new IOException("The provided stream is not readable");
|
||||
}
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return base.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] bytes) throws IOException {
|
||||
return base.read(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
||||
return base.read(bytes, i, i1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long l) throws IOException {
|
||||
return base.skip(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return base.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
base.dispose();
|
||||
}
|
||||
}
|
||||
676
app/src/main/java/us/shandian/giga/service/DownloadManager.java
Normal file
676
app/src/main/java/us/shandian/giga/service/DownloadManager.java
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
177
app/src/main/java/us/shandian/giga/ui/common/Deleter.java
Normal file
177
app/src/main/java/us/shandian/giga/ui/common/Deleter.java
Normal file
@ -0,0 +1,177 @@
|
||||
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;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManager.MissionIterator;
|
||||
import us.shandian.giga.ui.adapter.MissionAdapter;
|
||||
|
||||
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;
|
||||
private boolean running = true;
|
||||
|
||||
private Context mContext;
|
||||
private MissionAdapter mAdapter;
|
||||
private DownloadManager mDownloadManager;
|
||||
private MissionIterator mIterator;
|
||||
private Handler mHandler;
|
||||
private View mView;
|
||||
|
||||
private final Runnable rShow;
|
||||
private final Runnable rNext;
|
||||
private final Runnable rCommit;
|
||||
|
||||
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
||||
mView = v;
|
||||
mContext = c;
|
||||
mAdapter = a;
|
||||
mDownloadManager = d;
|
||||
mIterator = i;
|
||||
mHandler = h;
|
||||
|
||||
// use variables to know the reference of the lambdas
|
||||
rShow = this::show;
|
||||
rNext = this::next;
|
||||
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) {
|
||||
mIterator.hide(item);
|
||||
items.add(0, item);
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
private void forget() {
|
||||
mIterator.unHide(items.remove(0));
|
||||
mAdapter.applyChanges();
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
private void show() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
running = true;
|
||||
|
||||
mHandler.postDelayed(rNext, DELAY);
|
||||
}
|
||||
|
||||
private void next() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
|
||||
|
||||
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||
snackbar.setAction(R.string.undo, s -> forget());
|
||||
snackbar.setActionTextColor(Color.YELLOW);
|
||||
snackbar.show();
|
||||
|
||||
mHandler.postDelayed(rCommit, TIMEOUT);
|
||||
}
|
||||
|
||||
private void commit() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
while (items.size() > 0) {
|
||||
Mission mission = items.remove(0);
|
||||
if (mission.deleted) continue;
|
||||
|
||||
mIterator.unHide(mission);
|
||||
mDownloadManager.deleteMission(mission);
|
||||
|
||||
if (mission instanceof FinishedMission) {
|
||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (items.size() < 1) {
|
||||
pause();
|
||||
return;
|
||||
}
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
private void pause() {
|
||||
running = false;
|
||||
mHandler.removeCallbacks(rNext);
|
||||
mHandler.removeCallbacks(rShow);
|
||||
mHandler.removeCallbacks(rCommit);
|
||||
if (snackbar != null) snackbar.dismiss();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
if (running) return;
|
||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(Bundle bundle) {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (bundle == null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,36 @@
|
||||
package us.shandian.giga.ui.common;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.ColorRes;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
public class ProgressDrawable extends Drawable {
|
||||
private float mProgress;
|
||||
private final int mBackgroundColor;
|
||||
private final int mForegroundColor;
|
||||
private static final int MARQUEE_INTERVAL = 150;
|
||||
|
||||
public ProgressDrawable(Context context, @ColorRes int background, @ColorRes int foreground) {
|
||||
this(ContextCompat.getColor(context, background), ContextCompat.getColor(context, foreground));
|
||||
private float mProgress;
|
||||
private int mBackgroundColor, mForegroundColor;
|
||||
private Handler mMarqueeHandler;
|
||||
private float mMarqueeProgress;
|
||||
private Path mMarqueeLine;
|
||||
private int mMarqueeSize;
|
||||
private long mMarqueeNext;
|
||||
|
||||
public ProgressDrawable() {
|
||||
mMarqueeLine = null;// marquee disabled
|
||||
mMarqueeProgress = 0f;
|
||||
mMarqueeSize = 0;
|
||||
mMarqueeNext = 0;
|
||||
}
|
||||
|
||||
public ProgressDrawable(int background, int foreground) {
|
||||
public void setColors(@ColorInt int background, @ColorInt int foreground) {
|
||||
mBackgroundColor = background;
|
||||
mForegroundColor = foreground;
|
||||
}
|
||||
@ -29,10 +40,20 @@ public class ProgressDrawable extends Drawable {
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void setMarquee(boolean marquee) {
|
||||
if (marquee == (mMarqueeLine != null)) {
|
||||
return;
|
||||
}
|
||||
mMarqueeLine = marquee ? new Path() : null;
|
||||
mMarqueeHandler = marquee ? new Handler(Looper.getMainLooper()) : null;
|
||||
mMarqueeSize = 0;
|
||||
mMarqueeNext = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
int width = canvas.getWidth();
|
||||
int height = canvas.getHeight();
|
||||
int width = getBounds().width();
|
||||
int height = getBounds().height();
|
||||
|
||||
Paint paint = new Paint();
|
||||
|
||||
@ -40,6 +61,42 @@ public class ProgressDrawable extends Drawable {
|
||||
canvas.drawRect(0, 0, width, height, paint);
|
||||
|
||||
paint.setColor(mForegroundColor);
|
||||
|
||||
if (mMarqueeLine != null) {
|
||||
if (mMarqueeSize < 1) setupMarquee(width, height);
|
||||
|
||||
int size = mMarqueeSize;
|
||||
Paint paint2 = new Paint();
|
||||
paint2.setColor(mForegroundColor);
|
||||
paint2.setStrokeWidth(size);
|
||||
paint2.setStyle(Paint.Style.STROKE);
|
||||
|
||||
size *= 2;
|
||||
|
||||
if (mMarqueeProgress >= size) {
|
||||
mMarqueeProgress = 1;
|
||||
} else {
|
||||
mMarqueeProgress++;
|
||||
}
|
||||
|
||||
// render marquee
|
||||
width += size * 2;
|
||||
Path marquee = new Path();
|
||||
for (float i = -size; i < width; i += size) {
|
||||
marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0);
|
||||
}
|
||||
marquee.close();
|
||||
|
||||
canvas.drawPath(marquee, paint2);// draw marquee
|
||||
|
||||
if (System.currentTimeMillis() >= mMarqueeNext) {
|
||||
// program next update
|
||||
mMarqueeNext = System.currentTimeMillis() + MARQUEE_INTERVAL;
|
||||
mMarqueeHandler.postDelayed(this::invalidateSelf, MARQUEE_INTERVAL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.drawRect(0, 0, (int) (mProgress * width), height, paint);
|
||||
}
|
||||
|
||||
@ -58,4 +115,17 @@ public class ProgressDrawable extends Drawable {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBoundsChange(Rect rect) {
|
||||
if (mMarqueeLine != null) setupMarquee(rect.width(), rect.height());
|
||||
}
|
||||
|
||||
private void setupMarquee(int width, int height) {
|
||||
mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width
|
||||
|
||||
mMarqueeLine.rewind();
|
||||
mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize);
|
||||
mMarqueeLine.lineTo(-mMarqueeSize * 4, height + mMarqueeSize);
|
||||
mMarqueeLine.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
package us.shandian.giga.ui.fragment;
|
||||
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
public class AllMissionsFragment extends MissionsFragment {
|
||||
|
||||
@Override
|
||||
protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) {
|
||||
return binder.getDownloadManager();
|
||||
}
|
||||
}
|
||||
@ -10,52 +10,58 @@ import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DeleteDownloadManager;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.DMBinder;
|
||||
import us.shandian.giga.ui.adapter.MissionAdapter;
|
||||
|
||||
public abstract class MissionsFragment extends Fragment {
|
||||
private DownloadManager mDownloadManager;
|
||||
private DownloadManagerService.DMBinder mBinder;
|
||||
public class MissionsFragment extends Fragment {
|
||||
|
||||
private static final int SPAN_SIZE = 2;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
private boolean mLinear;
|
||||
private MenuItem mSwitch;
|
||||
private MenuItem mClear = null;
|
||||
|
||||
private RecyclerView mList;
|
||||
private View mEmpty;
|
||||
private MissionAdapter mAdapter;
|
||||
private GridLayoutManager mGridManager;
|
||||
private LinearLayoutManager mLinearManager;
|
||||
private Context mActivity;
|
||||
private DeleteDownloadManager mDeleteDownloadManager;
|
||||
private Disposable mDeleteDisposable;
|
||||
|
||||
private final ServiceConnection mConnection = new ServiceConnection() {
|
||||
private DMBinder mBinder;
|
||||
private Bundle mBundle;
|
||||
private boolean mForceUpdate;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||
mDownloadManager = setupDownloadManager(mBinder);
|
||||
if (mDeleteDownloadManager != null) {
|
||||
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
|
||||
updateList();
|
||||
}
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty);
|
||||
mAdapter.deleterLoad(mBundle, getView());
|
||||
|
||||
mBundle = null;
|
||||
|
||||
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
||||
mBinder.enableNotifications(false);
|
||||
|
||||
updateList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -66,14 +72,6 @@ public abstract class MissionsFragment extends Fragment {
|
||||
|
||||
};
|
||||
|
||||
public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) {
|
||||
mDeleteDownloadManager = deleteDownloadManager;
|
||||
if (mDownloadManager != null) {
|
||||
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
|
||||
updateList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.missions, container, false);
|
||||
@ -81,18 +79,32 @@ public abstract class MissionsFragment extends Fragment {
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
mLinear = mPrefs.getBoolean("linear", false);
|
||||
|
||||
mActivity = getActivity();
|
||||
mBundle = savedInstanceState;
|
||||
|
||||
// Bind the service
|
||||
Intent i = new Intent();
|
||||
i.setClass(getActivity(), DownloadManagerService.class);
|
||||
getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
|
||||
mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
// Views
|
||||
mEmpty = v.findViewById(R.id.list_empty_view);
|
||||
mList = v.findViewById(R.id.mission_recycler);
|
||||
|
||||
// Init
|
||||
mGridManager = new GridLayoutManager(getActivity(), 2);
|
||||
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
|
||||
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
switch (mAdapter.getItemViewType(position)) {
|
||||
case DownloadManager.SPECIAL_PENDING:
|
||||
case DownloadManager.SPECIAL_FINISHED:
|
||||
return SPAN_SIZE;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mLinearManager = new LinearLayoutManager(getActivity());
|
||||
mList.setLayoutManager(mGridManager);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
@ -123,31 +135,26 @@ public abstract class MissionsFragment extends Fragment {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (mDeleteDownloadManager != null) {
|
||||
mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> {
|
||||
if (mAdapter != null) {
|
||||
mAdapter.updateItemList();
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
getActivity().unbindService(mConnection);
|
||||
if (mDeleteDisposable != null) {
|
||||
mDeleteDisposable.dispose();
|
||||
}
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mBinder == null || mAdapter == null) return;
|
||||
|
||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||
mBinder.enableNotifications(true);
|
||||
mActivity.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(null);
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@ -155,35 +162,71 @@ public abstract class MissionsFragment extends Fragment {
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.switch_mode:
|
||||
mLinear = !mLinear;
|
||||
updateList();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyChange() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
mLinear = !mLinear;
|
||||
updateList();
|
||||
return true;
|
||||
case R.id.clear_list:
|
||||
mAdapter.clearFinishedDownloads();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear);
|
||||
|
||||
if (mLinear) {
|
||||
mList.setLayoutManager(mLinearManager);
|
||||
} else {
|
||||
mList.setLayoutManager(mGridManager);
|
||||
}
|
||||
|
||||
// destroy all created views in the recycler
|
||||
mList.setAdapter(null);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
|
||||
// re-attach the adapter in grid/lineal mode
|
||||
mAdapter.setLinear(mLinear);
|
||||
mList.setAdapter(mAdapter);
|
||||
|
||||
if (mSwitch != null) {
|
||||
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
|
||||
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
||||
mPrefs.edit().putBoolean("linear", mLinear).apply();
|
||||
}
|
||||
|
||||
mPrefs.edit().putBoolean("linear", mLinear).apply();
|
||||
}
|
||||
|
||||
protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(outState);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterResume();
|
||||
|
||||
if (mForceUpdate) {
|
||||
mForceUpdate = false;
|
||||
mAdapter.forceUpdate();
|
||||
}
|
||||
|
||||
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
||||
}
|
||||
if (mBinder != null) mBinder.enableNotifications(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mAdapter != null) mAdapter.onPaused();
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,15 +3,18 @@ package us.shandian.giga.util;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.ColorRes;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
@ -19,14 +22,17 @@ import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Utility {
|
||||
|
||||
public enum FileType {
|
||||
VIDEO,
|
||||
MUSIC,
|
||||
SUBTITLE,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@ -34,11 +40,11 @@ public class Utility {
|
||||
if (bytes < 1024) {
|
||||
return String.format("%d B", bytes);
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f kB", (float) bytes / 1024);
|
||||
return String.format("%.2f kB", bytes / 1024d);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", (float) bytes / 1024 / 1024);
|
||||
return String.format("%.2f MB", bytes / 1024d / 1024d);
|
||||
} else {
|
||||
return String.format("%.2f GB", (float) bytes / 1024 / 1024 / 1024);
|
||||
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,41 +60,32 @@ public class Utility {
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeToFile(@NonNull String fileName, @NonNull Serializable serializable) {
|
||||
ObjectOutputStream objectOutputStream = null;
|
||||
public static void writeToFile(@NonNull File file, @NonNull Serializable serializable) {
|
||||
|
||||
try {
|
||||
objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)));
|
||||
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
objectOutputStream.writeObject(serializable);
|
||||
} catch (Exception e) {
|
||||
//nothing to do
|
||||
} finally {
|
||||
if(objectOutputStream != null) {
|
||||
try {
|
||||
objectOutputStream.close();
|
||||
} catch (Exception e) {
|
||||
//nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
//nothing to do
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T readFromFile(String file) {
|
||||
T object = null;
|
||||
public static <T> T readFromFile(File file) {
|
||||
T object;
|
||||
ObjectInputStream objectInputStream = null;
|
||||
|
||||
try {
|
||||
objectInputStream = new ObjectInputStream(new FileInputStream(file));
|
||||
object = (T) objectInputStream.readObject();
|
||||
} catch (Exception e) {
|
||||
//nothing to do
|
||||
object = null;
|
||||
}
|
||||
|
||||
if(objectInputStream != null){
|
||||
if (objectInputStream != null) {
|
||||
try {
|
||||
objectInputStream .close();
|
||||
objectInputStream.close();
|
||||
} catch (Exception e) {
|
||||
//nothing to do
|
||||
}
|
||||
@ -119,39 +116,68 @@ public class Utility {
|
||||
}
|
||||
}
|
||||
|
||||
public static FileType getFileType(String file) {
|
||||
if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a")) {
|
||||
public static FileType getFileType(char kind, String file) {
|
||||
switch (kind) {
|
||||
case 'v':
|
||||
return FileType.VIDEO;
|
||||
case 'a':
|
||||
return FileType.MUSIC;
|
||||
case 's':
|
||||
return FileType.SUBTITLE;
|
||||
//default '?':
|
||||
}
|
||||
|
||||
if (file.endsWith(".srt") || file.endsWith(".vtt") || file.endsWith(".ssa")) {
|
||||
return FileType.SUBTITLE;
|
||||
} else if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a") || file.endsWith(".opus")) {
|
||||
return FileType.MUSIC;
|
||||
} else if (file.endsWith(".mp4") || file.endsWith(".mpeg") || file.endsWith(".rm") || file.endsWith(".rmvb")
|
||||
|| file.endsWith(".flv") || file.endsWith(".webp") || file.endsWith(".webm")) {
|
||||
return FileType.VIDEO;
|
||||
} else {
|
||||
return FileType.UNKNOWN;
|
||||
}
|
||||
|
||||
return FileType.UNKNOWN;
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
public static int getBackgroundForFileType(FileType type) {
|
||||
@ColorInt
|
||||
public static int getBackgroundForFileType(Context ctx, FileType type) {
|
||||
int colorRes;
|
||||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.color.audio_left_to_load_color;
|
||||
colorRes = R.color.audio_left_to_load_color;
|
||||
break;
|
||||
case VIDEO:
|
||||
return R.color.video_left_to_load_color;
|
||||
colorRes = R.color.video_left_to_load_color;
|
||||
break;
|
||||
case SUBTITLE:
|
||||
colorRes = R.color.subtitle_left_to_load_color;
|
||||
break;
|
||||
default:
|
||||
return R.color.gray;
|
||||
colorRes = R.color.gray;
|
||||
}
|
||||
|
||||
return ContextCompat.getColor(ctx, colorRes);
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
public static int getForegroundForFileType(FileType type) {
|
||||
@ColorInt
|
||||
public static int getForegroundForFileType(Context ctx, FileType type) {
|
||||
int colorRes;
|
||||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.color.audio_already_load_color;
|
||||
colorRes = R.color.audio_already_load_color;
|
||||
break;
|
||||
case VIDEO:
|
||||
return R.color.video_already_load_color;
|
||||
colorRes = R.color.video_already_load_color;
|
||||
break;
|
||||
case SUBTITLE:
|
||||
colorRes = R.color.subtitle_already_load_color;
|
||||
break;
|
||||
default:
|
||||
return R.color.gray;
|
||||
colorRes = R.color.gray;
|
||||
break;
|
||||
}
|
||||
|
||||
return ContextCompat.getColor(ctx, colorRes);
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
@ -161,6 +187,8 @@ public class Utility {
|
||||
return R.drawable.music;
|
||||
case VIDEO:
|
||||
return R.drawable.video;
|
||||
case SUBTITLE:
|
||||
return R.drawable.subtitle;
|
||||
default:
|
||||
return R.drawable.video;
|
||||
}
|
||||
@ -168,12 +196,18 @@ public class Utility {
|
||||
|
||||
public static void copyToClipboard(Context context, String str) {
|
||||
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (cm == null) {
|
||||
Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
cm.setPrimaryClip(ClipData.newPlainText("text", str));
|
||||
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public static String checksum(String path, String algorithm) {
|
||||
MessageDigest md = null;
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance(algorithm);
|
||||
@ -181,7 +215,7 @@ public class Utility {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
FileInputStream i = null;
|
||||
FileInputStream i;
|
||||
|
||||
try {
|
||||
i = new FileInputStream(path);
|
||||
@ -190,14 +224,14 @@ public class Utility {
|
||||
}
|
||||
|
||||
byte[] buf = new byte[1024];
|
||||
int len = 0;
|
||||
int len;
|
||||
|
||||
try {
|
||||
while ((len = i.read(buf)) != -1) {
|
||||
md.update(buf, 0, len);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
byte[] digest = md.digest();
|
||||
@ -211,4 +245,31 @@ public class Utility {
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static boolean mkdir(File path, boolean allDirs) {
|
||||
if (path.exists()) return true;
|
||||
|
||||
if (allDirs)
|
||||
path.mkdirs();
|
||||
else
|
||||
path.mkdir();
|
||||
|
||||
return path.exists();
|
||||
}
|
||||
|
||||
public static long getContentLength(HttpURLConnection connection) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return connection.getContentLengthLong();
|
||||
}
|
||||
|
||||
try {
|
||||
long length = Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||
if (length >= 0) return length;
|
||||
} catch (Exception err) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/subtitle.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/subtitle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@ -53,6 +53,12 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/subtitle_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/caption_setting_title"/>
|
||||
</RadioGroup>
|
||||
|
||||
<Spinner
|
||||
@ -77,6 +83,7 @@
|
||||
android:text="@string/msg_threads"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/threads_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/threads_text_view"
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="1dp"
|
||||
android:src="@drawable/ic_menu_more"
|
||||
android:scaleType="centerInside"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="TODO" />
|
||||
|
||||
</RelativeLayout>
|
||||
@ -61,7 +61,9 @@
|
||||
android:layout_below="@id/item_icon"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:text="XXX.xx"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
android:layout_toRightOf="@id/item_size"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:textStyle="bold"
|
||||
android:text="0%"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<include
|
||||
layout="@layout/list_empty_view"
|
||||
android:id="@+id/list_empty_view"
|
||||
android:visibility="gone" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/mission_recycler"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
30
app/src/main/res/layout/missions_header.xml
Normal file
30
app/src/main/res/layout/missions_header.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:text="relative header"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="@color/black_settings_accent_color" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -2,10 +2,18 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/switch_mode"
|
||||
android:icon="@drawable/list"
|
||||
android:title="@string/grid"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item android:id="@+id/action_settings"
|
||||
app:showAsAction="never"
|
||||
android:title="@string/settings"/>
|
||||
<item android:id="@+id/switch_mode"
|
||||
|
||||
<item android:id="@+id/clear_list"
|
||||
android:visible="false"
|
||||
android:icon="@drawable/ic_delete_sweep_white_24dp"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/switch_view"/>
|
||||
</menu>
|
||||
android:title="@string/clear_finished_download"/>
|
||||
</menu>
|
||||
|
||||
@ -1,33 +1,48 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/start"
|
||||
android:title="@string/start" />
|
||||
|
||||
<item
|
||||
android:id="@+id/start"
|
||||
android:title="@string/start"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/pause"
|
||||
android:title="@string/pause"/>
|
||||
<item
|
||||
android:id="@+id/pause"
|
||||
android:title="@string/pause" />
|
||||
|
||||
<item
|
||||
android:id="@+id/delete"
|
||||
android:title="@string/delete"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/checksum"
|
||||
android:title="@string/checksum">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/md5"
|
||||
android:title="@string/md5"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/sha1"
|
||||
android:title="@string/sha1"/>
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/queue"
|
||||
android:title="@string/enqueue"
|
||||
android:checkable="true"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/open"
|
||||
android:title="@string/view" />
|
||||
|
||||
<item
|
||||
android:id="@+id/delete"
|
||||
android:title="@string/delete" />
|
||||
|
||||
<item
|
||||
android:id="@+id/error_message_view"
|
||||
android:title="@string/show_error" />
|
||||
|
||||
<item
|
||||
android:id="@+id/source"
|
||||
android:title="@string/show_info" />
|
||||
|
||||
<item
|
||||
android:id="@+id/checksum"
|
||||
android:title="@string/checksum">
|
||||
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/md5"
|
||||
android:title="@string/md5" />
|
||||
|
||||
<item
|
||||
android:id="@+id/sha1"
|
||||
android:title="@string/sha1" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
</menu>
|
||||
|
||||
@ -500,4 +500,55 @@ abrir en modo popup</string>
|
||||
<string name="users">Usuarios</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="paused">pausado</string>
|
||||
<string name="queued">en cola</string>
|
||||
<string name="post_processing">post-procesado</string>
|
||||
|
||||
<string name="enqueue">Encolar</string>
|
||||
|
||||
<string name="permission_denied">Acción denegada por el sistema</string>
|
||||
|
||||
<string name="file_deleted">Archivo borrado</string>
|
||||
|
||||
<!-- download notifications -->
|
||||
<string name="download_failed">Descarga fallida</string>
|
||||
<string name="download_finished">Descarga finalizada</string>
|
||||
<string name="download_finished_more">%s descargas finalizadas</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="download_already_running">Hay una descarga en curso 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">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string>
|
||||
<string name="stop">Detener</string>
|
||||
<string name="max_retry_msg">Intentos maximos</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">No todas las descargas se pueden suspender, en esos casos, se reiniciaran</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_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 puede 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">Rango solicitado no satisfactorio</string>
|
||||
<string name="error_http_not_found">No encontrado</string>
|
||||
<string name="error_postprocessing_failed">Fallo el post-procesado</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@ -63,6 +63,8 @@
|
||||
<color name="audio_already_load_color">#000000</color>
|
||||
<color name="video_left_to_load_color">#CD5656</color>
|
||||
<color name="video_already_load_color">#BC211D</color>
|
||||
<color name="subtitle_left_to_load_color">#008ea4</color>
|
||||
<color name="subtitle_already_load_color">#005a71</color>
|
||||
|
||||
<!-- GigaGet Component colors -->
|
||||
<color name="white">#FFFFFF</color>
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
|
||||
<string name="use_external_audio_player_key" translatable="false">use_external_audio_player</string>
|
||||
<string name="autoplay_through_intent_key" translatable="false">autoplay_through_intent</string>
|
||||
<string name="use_old_player_key" translatable="false">use_oldplayer</string>
|
||||
|
||||
<string name="volume_gesture_control_key" translatable="false">volume_gesture_control</string>
|
||||
<string name="brightness_gesture_control_key" translatable="false">brightness_gesture_control</string>
|
||||
@ -173,6 +174,24 @@
|
||||
</string-array>
|
||||
|
||||
<string name="default_file_charset_value" translatable="false">@string/charset_most_special_characters_value</string>
|
||||
|
||||
<string name="downloads_maximum_retry" translatable="false">downloads_max_retry</string>
|
||||
<string name="downloads_maximum_retry_default" translatable="false">3</string>
|
||||
<string-array name="downloads_maximum_retry_list" translatable="false">
|
||||
<item translatable="true">@string/minimize_on_exit_none_description</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>7</item>
|
||||
<item>10</item>
|
||||
<item>15</item>
|
||||
</string-array>
|
||||
|
||||
<string name="downloads_cross_network" translatable="false">cross_network_downloads</string>
|
||||
|
||||
<string name="default_download_threads" translatable="false">default_download_threads</string>
|
||||
|
||||
<!-- Preferred action on open (open from external app) -->
|
||||
<string name="preferred_open_action_key" translatable="false">preferred_open_action_key</string>
|
||||
|
||||
@ -143,6 +143,7 @@
|
||||
<string name="popup_resizing_indicator_title">Resizing</string>
|
||||
<string name="best_resolution">Best resolution</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="file_deleted">File deleted</string>
|
||||
<string name="play_all">Play All</string>
|
||||
<string name="always">Always</string>
|
||||
<string name="just_once">Just Once</string>
|
||||
@ -523,4 +524,51 @@
|
||||
<string name="grid">Grid</string>
|
||||
<string name="auto">Auto</string>
|
||||
<string name="switch_view">Switch View</string>
|
||||
|
||||
|
||||
<string name="missions_header_finished">Finished</string>
|
||||
<string name="missions_header_pending">In queue</string>
|
||||
|
||||
<string name="paused">paused</string>
|
||||
<string name="queued">queued</string>
|
||||
<string name="post_processing">post-processing</string>
|
||||
|
||||
<string name="enqueue">Queue</string>
|
||||
|
||||
<string name="permission_denied">Action denied by the system</string>
|
||||
|
||||
<!-- download notifications -->
|
||||
<string name="download_failed">Download failed</string>
|
||||
<string name="download_finished">Download finished</string>
|
||||
<string name="download_finished_more">%s downloads finished</string>
|
||||
|
||||
<!-- 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="download_already_running">There is a download in progress 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_permission_denied">Permission denied by the system</string>
|
||||
<string name="error_ssl_exception">Secure connection failed</string>
|
||||
<string name="error_unknown_host">Can not found the server</string>
|
||||
<string name="error_connect_host">Can not connect to the server</string>
|
||||
<string name="error_http_no_content">The server does not send data</string>
|
||||
<string name="error_http_unsupported_range">The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Requested Range Not Satisfiable</string>
|
||||
<string name="error_http_not_found">Not found</string>
|
||||
<string name="error_postprocessing_failed">Post-processing failed</string>
|
||||
|
||||
<string name="clear_finished_download">Clear finished downloads</string>
|
||||
<string name="msg_pending_downloads">You have %s pending downloads, goto Downloads to continue</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="max_retry_msg">Maximum retry</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">Not all downloads can be suspended, in those cases, will be restarted</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@ -29,4 +29,18 @@
|
||||
android:summary="@string/settings_file_replacement_character_summary"
|
||||
android:title="@string/settings_file_replacement_character_title"/>
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/downloads_maximum_retry_default"
|
||||
android:entries="@array/downloads_maximum_retry_list"
|
||||
android:entryValues="@array/downloads_maximum_retry_list"
|
||||
android:key="@string/downloads_maximum_retry"
|
||||
android:summary="@string/max_retry_desc"
|
||||
android:title="@string/max_retry_msg" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/downloads_cross_network"
|
||||
android:summary="@string/pause_downloads_on_mobile_desc"
|
||||
android:title="@string/pause_downloads_on_mobile" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadDataSource;
|
||||
import us.shandian.giga.get.DownloadManagerImpl;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test for {@link DownloadManagerImpl}
|
||||
*
|
||||
* TODO: test loading from .giga files, startMission and improve tests
|
||||
*/
|
||||
public class DownloadManagerImplTest {
|
||||
|
||||
private DownloadManagerImpl downloadManager;
|
||||
private DownloadDataSource downloadDataSource;
|
||||
private ArrayList<DownloadMission> missions;
|
||||
|
||||
@org.junit.Before
|
||||
public void setUp() throws Exception {
|
||||
downloadDataSource = mock(DownloadDataSource.class);
|
||||
missions = new ArrayList<>();
|
||||
for(int i = 0; i < 50; ++i){
|
||||
missions.add(generateFinishedDownloadMission());
|
||||
}
|
||||
when(downloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions));
|
||||
downloadManager = new DownloadManagerImpl(new ArrayList<>(), downloadDataSource);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void testConstructorWithNullAsDownloadDataSource() {
|
||||
new DownloadManagerImpl(new ArrayList<>(), null);
|
||||
}
|
||||
|
||||
|
||||
private static DownloadMission generateFinishedDownloadMission() throws IOException {
|
||||
File file = File.createTempFile("newpipetest", ".mp4");
|
||||
file.deleteOnExit();
|
||||
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
|
||||
randomAccessFile.setLength(1000);
|
||||
randomAccessFile.close();
|
||||
DownloadMission downloadMission = new DownloadMission(file.getName(),
|
||||
"http://google.com/?q=how+to+google", file.getParent());
|
||||
downloadMission.blocks = 1000;
|
||||
downloadMission.done = 1000;
|
||||
downloadMission.finished = true;
|
||||
return spy(downloadMission);
|
||||
}
|
||||
|
||||
private static void assertMissionEquals(String message, DownloadMission expected, DownloadMission actual) {
|
||||
if(expected == actual) return;
|
||||
assertEquals(message + ": Name", expected.name, actual.name);
|
||||
assertEquals(message + ": Location", expected.location, actual.location);
|
||||
assertEquals(message + ": Url", expected.url, actual.url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatMissionsAreLoaded() throws IOException {
|
||||
ArrayList<DownloadMission> missions = new ArrayList<>();
|
||||
long millis = System.currentTimeMillis();
|
||||
for(int i = 0; i < 50; ++i){
|
||||
DownloadMission mission = generateFinishedDownloadMission();
|
||||
mission.timestamp = millis - i; // reverse order by timestamp
|
||||
missions.add(mission);
|
||||
}
|
||||
|
||||
downloadDataSource = mock(DownloadDataSource.class);
|
||||
when(downloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions));
|
||||
downloadManager = new DownloadManagerImpl(new ArrayList<>(), downloadDataSource);
|
||||
verify(downloadDataSource, times(1)).loadMissions();
|
||||
|
||||
assertEquals(50, downloadManager.getCount());
|
||||
|
||||
for(int i = 0; i < 50; ++i) {
|
||||
assertMissionEquals("mission " + i, missions.get(50 - 1 - i), downloadManager.getMission(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void startMission() throws Exception {
|
||||
DownloadMission mission = missions.get(0);
|
||||
mission = spy(mission);
|
||||
missions.set(0, mission);
|
||||
String url = "https://github.com/favicon.ico";
|
||||
// create a temp file and delete it so we have a temp directory
|
||||
File tempFile = File.createTempFile("favicon",".ico");
|
||||
String name = tempFile.getName();
|
||||
String location = tempFile.getParent();
|
||||
assertTrue(tempFile.delete());
|
||||
int id = downloadManager.startMission(url, location, name, true, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumeMission() {
|
||||
DownloadMission mission = missions.get(0);
|
||||
mission.running = true;
|
||||
verify(mission, never()).start();
|
||||
downloadManager.resumeMission(0);
|
||||
verify(mission, never()).start();
|
||||
mission.running = false;
|
||||
downloadManager.resumeMission(0);
|
||||
verify(mission, times(1)).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pauseMission() {
|
||||
DownloadMission mission = missions.get(0);
|
||||
mission.running = false;
|
||||
downloadManager.pauseMission(0);
|
||||
verify(mission, never()).pause();
|
||||
mission.running = true;
|
||||
downloadManager.pauseMission(0);
|
||||
verify(mission, times(1)).pause();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteMission() {
|
||||
DownloadMission mission = missions.get(0);
|
||||
assertEquals(mission, downloadManager.getMission(0));
|
||||
downloadManager.deleteMission(0);
|
||||
verify(mission, times(1)).delete();
|
||||
assertNotEquals(mission, downloadManager.getMission(0));
|
||||
assertEquals(49, downloadManager.getCount());
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void getMissionWithNegativeIndex() {
|
||||
downloadManager.getMission(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMission() {
|
||||
assertSame(missions.get(0), downloadManager.getMission(0));
|
||||
assertSame(missions.get(1), downloadManager.getMission(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortByTimestamp() {
|
||||
ArrayList<DownloadMission> downloadMissions = new ArrayList<>();
|
||||
DownloadMission mission = new DownloadMission();
|
||||
mission.timestamp = 0;
|
||||
|
||||
DownloadMission mission1 = new DownloadMission();
|
||||
mission1.timestamp = Integer.MAX_VALUE + 1L;
|
||||
|
||||
DownloadMission mission2 = new DownloadMission();
|
||||
mission2.timestamp = 2L * Integer.MAX_VALUE ;
|
||||
|
||||
DownloadMission mission3 = new DownloadMission();
|
||||
mission3.timestamp = 2L * Integer.MAX_VALUE + 5L;
|
||||
|
||||
|
||||
downloadMissions.add(mission3);
|
||||
downloadMissions.add(mission1);
|
||||
downloadMissions.add(mission2);
|
||||
downloadMissions.add(mission);
|
||||
|
||||
|
||||
DownloadManagerImpl.sortByTimestamp(downloadMissions);
|
||||
|
||||
assertEquals(mission, downloadMissions.get(0));
|
||||
assertEquals(mission1, downloadMissions.get(1));
|
||||
assertEquals(mission2, downloadMissions.get(2));
|
||||
assertEquals(mission3, downloadMissions.get(3));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user