Implement Storage Access Framework
* re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
parent
9e34fee58c
commit
f6b32823ba
62 changed files with 2439 additions and 1180 deletions
|
@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||||
private void updateFragments() {
|
private void updateFragments() {
|
||||||
MissionsFragment fragment = new MissionsFragment();
|
MissionsFragment fragment = new MissionsFragment();
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
.commit();
|
.commit();
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package org.schabi.newpipe.download;
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.IdRes;
|
import android.support.annotation.IdRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
@ -14,6 +20,7 @@ import android.support.v7.widget.Toolbar;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
|
@ -35,7 +42,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.extractor.utils.Localization;
|
import org.schabi.newpipe.extractor.utils.Localization;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.FilenameUtils;
|
import org.schabi.newpipe.util.FilenameUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
@ -44,20 +52,27 @@ import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
|
import us.shandian.giga.service.DownloadManager;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||||
|
import us.shandian.giga.service.MissionState;
|
||||||
|
|
||||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||||
private static final String TAG = "DialogFragment";
|
private static final String TAG = "DialogFragment";
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected StreamInfo currentInfo;
|
protected StreamInfo currentInfo;
|
||||||
|
@ -82,7 +97,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
|
|
||||||
private EditText nameEditText;
|
private EditText nameEditText;
|
||||||
private Spinner streamsSpinner;
|
private Spinner streamsSpinner;
|
||||||
private RadioGroup radioVideoAudioGroup;
|
private RadioGroup radioStreamsGroup;
|
||||||
private TextView threadsCountTextView;
|
private TextView threadsCountTextView;
|
||||||
private SeekBar threadsSeekBar;
|
private SeekBar threadsSeekBar;
|
||||||
|
|
||||||
|
@ -162,7 +177,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
final Context context = getContext();
|
||||||
|
if (context == null)
|
||||||
|
throw new RuntimeException("Context was null");
|
||||||
|
|
||||||
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||||
|
@ -179,9 +198,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
|
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
||||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||||
|
context.startService(intent);
|
||||||
|
|
||||||
|
context.bindService(intent, new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||||
|
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||||
|
|
||||||
|
mainStorageAudio = mgr.getMainStorageAudio();
|
||||||
|
mainStorageVideo = mgr.getMainStorageVideo();
|
||||||
|
downloadManager = mgr.getDownloadManager();
|
||||||
|
|
||||||
|
okButton.setEnabled(true);
|
||||||
|
|
||||||
|
context.unbindService(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}, Context.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -206,8 +248,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||||
threadsSeekBar = view.findViewById(R.id.threads);
|
threadsSeekBar = view.findViewById(R.id.threads);
|
||||||
|
|
||||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
initToolbar(view.findViewById(R.id.toolbar));
|
initToolbar(view.findViewById(R.id.toolbar));
|
||||||
setupDownloadOptions();
|
setupDownloadOptions();
|
||||||
|
@ -242,17 +284,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
|
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||||
setupVideoSpinner();
|
setupVideoSpinner();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||||
setupAudioSpinner();
|
setupAudioSpinner();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||||
setupSubtitleSpinner();
|
setupSubtitleSpinner();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -270,17 +312,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (data.getData() == null) {
|
||||||
|
showFailedDialog(R.string.general_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), ""));
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorActivity(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Inits
|
// Inits
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void initToolbar(Toolbar toolbar) {
|
private void initToolbar(Toolbar toolbar) {
|
||||||
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||||
|
|
||||||
|
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
|
||||||
|
okButton = toolbar.findViewById(R.id.okay);
|
||||||
|
okButton.setEnabled(false);// disable until the download service connection is done
|
||||||
|
|
||||||
toolbar.setTitle(R.string.download_dialog_title);
|
toolbar.setTitle(R.string.download_dialog_title);
|
||||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||||
toolbar.inflateMenu(R.menu.dialog_url);
|
toolbar.inflateMenu(R.menu.dialog_url);
|
||||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||||
|
|
||||||
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
if (item.getItemId() == R.id.okay) {
|
if (item.getItemId() == R.id.okay) {
|
||||||
prepareSelectedDownload();
|
prepareSelectedDownload();
|
||||||
|
@ -348,7 +413,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
if (DEBUG)
|
if (DEBUG)
|
||||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
selectedAudioIndex = position;
|
selectedAudioIndex = position;
|
||||||
break;
|
break;
|
||||||
|
@ -372,9 +437,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
protected void setupDownloadOptions() {
|
protected void setupDownloadOptions() {
|
||||||
setRadioButtonsState(false);
|
setRadioButtonsState(false);
|
||||||
|
|
||||||
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||||
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||||
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
|
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||||
|
@ -399,9 +464,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRadioButtonsState(boolean enabled) {
|
private void setRadioButtonsState(boolean enabled) {
|
||||||
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||||
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||||
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||||
|
@ -436,119 +501,248 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StoredDirectoryHelper mainStorageAudio = null;
|
||||||
|
StoredDirectoryHelper mainStorageVideo = null;
|
||||||
|
DownloadManager downloadManager = null;
|
||||||
|
|
||||||
|
MenuItem okButton = null;
|
||||||
|
|
||||||
|
private String getNameEditText() {
|
||||||
|
return nameEditText.getText().toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFailedDialog(@StringRes int msg) {
|
||||||
|
new AlertDialog.Builder(getContext())
|
||||||
|
.setMessage(msg)
|
||||||
|
.setNegativeButton(android.R.string.ok, null)
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorActivity(Exception e) {
|
||||||
|
ErrorActivity.reportError(
|
||||||
|
getContext(),
|
||||||
|
Collections.singletonList(e),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void prepareSelectedDownload() {
|
private void prepareSelectedDownload() {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
Stream stream;
|
StoredDirectoryHelper mainStorage;
|
||||||
String location;
|
MediaFormat format;
|
||||||
char kind;
|
String mime;
|
||||||
|
|
||||||
String fileName = nameEditText.getText().toString().trim();
|
// first, build the filename and get the output folder (if possible)
|
||||||
if (fileName.isEmpty())
|
|
||||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
|
||||||
|
|
||||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
String filename = getNameEditText() + ".";
|
||||||
|
if (filename.isEmpty()) {
|
||||||
|
filename = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||||
|
}
|
||||||
|
filename += ".";
|
||||||
|
|
||||||
|
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
mainStorage = mainStorageAudio;
|
||||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
kind = 'a';
|
mime = format.mimeType;
|
||||||
|
filename += format.suffix;
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
mainStorage = mainStorageVideo;
|
||||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
kind = 'v';
|
mime = format.mimeType;
|
||||||
|
filename += format.suffix;
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
kind = 's';
|
mime = format.mimeType;
|
||||||
|
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("No stream selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainStorage == null) {
|
||||||
|
// this part is called if...
|
||||||
|
// older android version running with SAF preferred
|
||||||
|
// save path not defined (via download settings)
|
||||||
|
|
||||||
|
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for existing file with the same name
|
||||||
|
Uri result = mainStorage.findFile(filename);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
// the file does not exists, create
|
||||||
|
StoredFileHelper storage = mainStorage.createFile(filename, mime);
|
||||||
|
if (storage == null || !storage.canWrite()) {
|
||||||
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continueSelectedDownload(storage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the target filename is already use, try load
|
||||||
|
StoredFileHelper storage;
|
||||||
|
try {
|
||||||
|
storage = new StoredFileHelper(context, result, mime);
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorActivity(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if is our file
|
||||||
|
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||||
|
@StringRes int msgBtn;
|
||||||
|
@StringRes int msgBody;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case Finished:
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.overwrite_finished_warning;
|
||||||
|
break;
|
||||||
|
case Pending:
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.download_already_pending;
|
||||||
|
break;
|
||||||
|
case PendingRunning:
|
||||||
|
msgBtn = R.string.generate_unique_name;
|
||||||
|
msgBody = R.string.download_already_running;
|
||||||
|
break;
|
||||||
|
case None:
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.overwrite_unrelated_warning;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int threads;
|
// handle user answer (overwrite or create another file with different name)
|
||||||
|
final String finalFilename = filename;
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
builder.setTitle(R.string.download_dialog_title)
|
||||||
|
.setMessage(msgBody)
|
||||||
|
.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
StoredFileHelper storageNew;
|
||||||
threads = 1;// use unique thread for subtitles due small file size
|
switch (state) {
|
||||||
fileName += ".srt";// final subtitle format
|
case Finished:
|
||||||
} else {
|
case Pending:
|
||||||
threads = threadsSeekBar.getProgress() + 1;
|
downloadManager.forgetMission(storage);
|
||||||
fileName += "." + stream.getFormat().getSuffix();
|
case None:
|
||||||
}
|
// try take (or steal) the file permissions
|
||||||
|
try {
|
||||||
final String finalFileName = fileName;
|
storageNew = new StoredFileHelper(context, result, mainStorage.getTag());
|
||||||
|
if (storageNew.canWrite())
|
||||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> {
|
continueSelectedDownload(storageNew);
|
||||||
@StringRes int msgBtn;
|
else
|
||||||
@StringRes int msgBody;
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
} catch (IOException e) {
|
||||||
switch (result) {
|
showErrorActivity(e);
|
||||||
case Finished:
|
}
|
||||||
msgBtn = R.string.overwrite;
|
break;
|
||||||
msgBody = R.string.overwrite_warning;
|
case PendingRunning:
|
||||||
break;
|
// FIXME: createUniqueFile() is not tested properly
|
||||||
case Pending:
|
storageNew = mainStorage.createUniqueFile(finalFilename, mime);
|
||||||
msgBtn = R.string.overwrite;
|
if (storageNew == null)
|
||||||
msgBody = R.string.download_already_pending;
|
showFailedDialog(R.string.error_file_creation);
|
||||||
break;
|
else
|
||||||
case PendingRunning:
|
continueSelectedDownload(storageNew);
|
||||||
msgBtn = R.string.generate_unique_name;
|
break;
|
||||||
msgBody = R.string.download_already_running;
|
}
|
||||||
break;
|
})
|
||||||
default:
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
.create()
|
||||||
return;
|
.show();
|
||||||
}
|
|
||||||
|
|
||||||
// overwrite or unique name actions are done by the download manager
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
||||||
builder.setTitle(R.string.download_dialog_title)
|
|
||||||
.setMessage(msgBody)
|
|
||||||
.setPositiveButton(
|
|
||||||
msgBtn,
|
|
||||||
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
|
|
||||||
)
|
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
||||||
|
final Context context = getContext();
|
||||||
|
|
||||||
|
if (!storage.canWrite()) {
|
||||||
|
showFailedDialog(R.string.permission_denied);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the selected file has to be overwritten, by simply checking its length
|
||||||
|
try {
|
||||||
|
if (storage.length() > 0) storage.truncate();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e);
|
||||||
|
//showErrorActivity(e);
|
||||||
|
showFailedDialog(R.string.overwrite_failed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream selectedStream;
|
||||||
|
char kind;
|
||||||
|
int threads = threadsSeekBar.getProgress() + 1;
|
||||||
String[] urls;
|
String[] urls;
|
||||||
String psName = null;
|
String psName = null;
|
||||||
String[] psArgs = null;
|
String[] psArgs = null;
|
||||||
String secondaryStreamUrl = null;
|
String secondaryStreamUrl = null;
|
||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
|
|
||||||
if (selectedStream instanceof AudioStream) {
|
// more download logic: select muxer, subtitle converter, etc.
|
||||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
case R.id.audio_button:
|
||||||
}
|
threads = 1;// use unique thread for subtitles due small file size
|
||||||
} else if (selectedStream instanceof VideoStream) {
|
kind = 'a';
|
||||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||||
.getAllSecondary()
|
|
||||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
|
||||||
|
|
||||||
if (secondaryStream != null) {
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_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 slow networks
|
|
||||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
|
||||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
case R.id.video_button:
|
||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
kind = 'v';
|
||||||
psArgs = new String[]{
|
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||||
selectedStream.getFormat().getSuffix(),
|
|
||||||
"false",// ignore empty frames
|
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||||
"false",// detect youtube duplicate lines
|
.getAllSecondary()
|
||||||
};
|
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||||
|
|
||||||
|
if (secondaryStream != null) {
|
||||||
|
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||||
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||||
|
else
|
||||||
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
|
|
||||||
|
psArgs = null;
|
||||||
|
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||||
|
|
||||||
|
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
||||||
|
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||||
|
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.subtitle_button:
|
||||||
|
kind = 's';
|
||||||
|
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||||
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
|
psArgs = new String[]{
|
||||||
|
selectedStream.getFormat().getSuffix(),
|
||||||
|
"false",// ignore empty frames
|
||||||
|
"false",// detect youtube duplicate lines
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStreamUrl == null) {
|
if (secondaryStreamUrl == null) {
|
||||||
|
@ -557,8 +751,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||||
|
|
||||||
getDialog().dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||||
* If chosen feed already displayed, then we request another feed from another
|
* If chosen feed already displayed, then we request another feed from another
|
||||||
* subscription, until the subscription table runs out of new items.
|
* subscription, until the subscription table runs out of new items.
|
||||||
* <p>
|
* <p>
|
||||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
* This Observer is self-contained and will close itself when complete. However, this
|
||||||
* does not obey the fragment lifecycle and may continue running in the background
|
* does not obey the fragment lifecycle and may continue running in the background
|
||||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||||
* an observer is unsubscribed while the thread process is still running.
|
* an observer is unsubscribed while the thread process is still running.
|
||||||
|
|
|
@ -158,7 +158,7 @@ public class MediaSourceManager {
|
||||||
* Dispose the manager and releases all message buses and loaders.
|
* Dispose the manager and releases all message buses and loaders.
|
||||||
* */
|
* */
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (DEBUG) Log.d(TAG, "dispose() called.");
|
if (DEBUG) Log.d(TAG, "close() called.");
|
||||||
|
|
||||||
debouncedSignal.onComplete();
|
debouncedSignal.onComplete();
|
||||||
debouncedLoader.dispose();
|
debouncedLoader.dispose();
|
||||||
|
|
|
@ -2,26 +2,42 @@ package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
import android.support.v7.preference.Preference;
|
import android.support.v7.preference.Preference;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||||
|
|
||||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
|
||||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||||
|
|
||||||
private String DOWNLOAD_PATH_PREFERENCE;
|
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||||
|
|
||||||
|
private String DOWNLOAD_STORAGE_API;
|
||||||
|
private String DOWNLOAD_STORAGE_API_DEFAULT;
|
||||||
|
|
||||||
|
private Preference prefPathVideo;
|
||||||
|
private Preference prefPathAudio;
|
||||||
|
|
||||||
|
private Context ctx;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -33,16 +49,100 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
addPreferencesFromResource(R.xml.download_settings);
|
addPreferencesFromResource(R.xml.download_settings);
|
||||||
|
|
||||||
|
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
|
||||||
|
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||||
|
|
||||||
|
updatePathPickers(usingJavaIO());
|
||||||
|
|
||||||
|
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> {
|
||||||
|
boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value);
|
||||||
|
|
||||||
|
if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
// forget save paths
|
||||||
|
forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE);
|
||||||
|
forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||||
|
|
||||||
|
defaultPreferences.edit()
|
||||||
|
.putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "")
|
||||||
|
.putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "")
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
updatePreferencesSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePathPickers(javaIO);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
ctx = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
ctx = null;
|
||||||
|
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initKeys() {
|
private void initKeys() {
|
||||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
|
||||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||||
|
DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api);
|
||||||
|
DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePreferencesSummary() {
|
private void updatePreferencesSummary() {
|
||||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
prefPathVideo.setSummary(
|
||||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary))
|
||||||
|
);
|
||||||
|
prefPathAudio.setSummary(
|
||||||
|
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePathPickers(boolean useJavaIO) {
|
||||||
|
boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||||
|
prefPathVideo.setEnabled(enabled);
|
||||||
|
prefPathAudio.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usingJavaIO() {
|
||||||
|
return DOWNLOAD_STORAGE_API_DEFAULT.equals(
|
||||||
|
defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private void forgetSAFTree(String prefKey) {
|
||||||
|
|
||||||
|
String oldPath = defaultPreferences.getString(prefKey, "");
|
||||||
|
|
||||||
|
if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) {
|
||||||
|
try {
|
||||||
|
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null);
|
||||||
|
if (!mainStorage.isDirect()) {
|
||||||
|
mainStorage.revokePermissions();
|
||||||
|
Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!");
|
||||||
|
}
|
||||||
|
} catch (IOException err) {
|
||||||
|
Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMessageDialog(@StringRes int title, @StringRes int message) {
|
||||||
|
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||||
|
msg.setTitle(title);
|
||||||
|
msg.setMessage(message);
|
||||||
|
msg.setPositiveButton(android.R.string.ok, null);
|
||||||
|
msg.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -51,17 +151,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|
String key = preference.getKey();
|
||||||
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
|
||||||
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
|
boolean safPick = !usingJavaIO();
|
||||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
|
|
||||||
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
|
int request = 0;
|
||||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
|
||||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
request = REQUEST_DOWNLOAD_VIDEO_PATH;
|
||||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
request = REQUEST_DOWNLOAD_AUDIO_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Intent i;
|
||||||
|
if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
} else {
|
||||||
|
i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||||
|
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(i, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onPreferenceTreeClick(preference);
|
return super.onPreferenceTreeClick(preference);
|
||||||
|
@ -71,25 +185,50 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
|
||||||
|
"resultCode = [" + resultCode + "], data = [" + data + "]"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
if (resultCode != Activity.RESULT_OK) return;
|
||||||
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
|
|
||||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
|
||||||
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
|
||||||
|
|
||||||
defaultPreferences.edit().putString(key, path).apply();
|
String key;
|
||||||
|
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
|
||||||
|
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||||
|
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||||
|
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri == null) {
|
||||||
|
showMessageDialog(R.string.general_error, R.string.invalid_directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// steps:
|
||||||
|
// 1. acquire permissions on the new save path
|
||||||
|
// 2. save the new path, if step(1) was successful
|
||||||
|
|
||||||
|
try {
|
||||||
|
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
|
||||||
|
mainStorage.acquirePermissions();
|
||||||
|
Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!");
|
||||||
|
} catch (IOException err) {
|
||||||
|
Log.e(TAG, "Error acquiring permissions on " + uri.toString());
|
||||||
|
showMessageDialog(R.string.general_error, R.string.no_available_dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPreferences.edit().putString(key, uri.toString()).apply();
|
||||||
|
} else {
|
||||||
|
defaultPreferences.edit().putString(key, uri.toString()).apply();
|
||||||
updatePreferencesSummary();
|
updatePreferencesSummary();
|
||||||
|
|
||||||
File target = new File(path);
|
File target = new File(URI.create(uri.toString()));
|
||||||
if (!target.canWrite()) {
|
if (!target.canWrite())
|
||||||
AlertDialog.Builder msg = new AlertDialog.Builder(getContext());
|
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
|
||||||
msg.setTitle(R.string.download_to_sdcard_error_title);
|
|
||||||
msg.setMessage(R.string.download_to_sdcard_error_message);
|
|
||||||
msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { });
|
|
||||||
msg.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,37 +70,23 @@ public class NewPipeSettings {
|
||||||
getAudioDownloadFolder(context);
|
getAudioDownloadFolder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getVideoDownloadFolder(Context context) {
|
private static void getVideoDownloadFolder(Context context) {
|
||||||
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
|
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getVideoDownloadPath(Context context) {
|
private static void getAudioDownloadFolder(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||||
final String key = context.getString(R.string.download_path_key);
|
|
||||||
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getAudioDownloadFolder(Context context) {
|
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||||
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAudioDownloadPath(Context context) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
final String key = context.getString(R.string.download_path_audio_key);
|
|
||||||
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String key = context.getString(keyID);
|
final String key = context.getString(keyID);
|
||||||
String downloadPath = prefs.getString(key, null);
|
String downloadPath = prefs.getString(key, null);
|
||||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
|
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
|
||||||
|
|
||||||
final File dir = getDir(defaultDirectoryName);
|
|
||||||
SharedPreferences.Editor spEditor = prefs.edit();
|
SharedPreferences.Editor spEditor = prefs.edit();
|
||||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
|
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||||
spEditor.apply();
|
spEditor.apply();
|
||||||
return dir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -110,8 +96,13 @@ public class NewPipeSettings {
|
||||||
|
|
||||||
public static void resetDownloadFolders(Context context) {
|
public static void resetDownloadFolders(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
prefs.edit()
|
||||||
|
.putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default))
|
||||||
|
.apply();
|
||||||
|
|
||||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
|
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
|
||||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
|
resetDownloadFolder(prefs, context.getString(R.string.download_path_video_key), Environment.DIRECTORY_MOVIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
|
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
|
||||||
|
|
|
@ -120,7 +120,7 @@ public class Mp4FromDashWriter {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
|
|
||||||
for (SharpStream src : sourceTracks) {
|
for (SharpStream src : sourceTracks) {
|
||||||
src.dispose();
|
src.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks = null;
|
tracks = null;
|
||||||
|
|
|
@ -107,7 +107,7 @@ public class WebMWriter {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
|
|
||||||
for (SharpStream src : sourceTracks) {
|
for (SharpStream src : sourceTracks) {
|
||||||
src.dispose();
|
src.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceTracks = null;
|
sourceTracks = null;
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package org.schabi.newpipe.streams.io;
|
package org.schabi.newpipe.streams.io;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* based on c#
|
* based on c#
|
||||||
*/
|
*/
|
||||||
public abstract class SharpStream {
|
public abstract class SharpStream implements Closeable {
|
||||||
|
|
||||||
public abstract int read() throws IOException;
|
public abstract int read() throws IOException;
|
||||||
|
|
||||||
|
@ -19,9 +20,10 @@ public abstract class SharpStream {
|
||||||
|
|
||||||
public abstract void rewind() throws IOException;
|
public abstract void rewind() throws IOException;
|
||||||
|
|
||||||
public abstract void dispose();
|
public abstract boolean isClosed();
|
||||||
|
|
||||||
public abstract boolean isDisposed();
|
@Override
|
||||||
|
public abstract void close();
|
||||||
|
|
||||||
public abstract boolean canRewind();
|
public abstract boolean canRewind();
|
||||||
|
|
||||||
|
@ -54,4 +56,8 @@ public abstract class SharpStream {
|
||||||
public void seek(long offset) throws IOException {
|
public void seek(long offset) throws IOException {
|
||||||
throw new IOException("Not implemented");
|
throw new IOException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long length() throws IOException {
|
||||||
|
throw new UnsupportedOperationException("Unsupported operation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ package us.shandian.giga.get;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
|
|
||||||
|
@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread {
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file;
|
SharpStream fs = mMission.storage.getStream();
|
||||||
if (mMission.current == 0) {
|
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||||
file = new File(mMission.location);
|
fs.seek(mMission.offsets[mMission.current]);
|
||||||
if (!Utility.mkdir(file, true)) {
|
fs.close();
|
||||||
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;
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package us.shandian.giga.get;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -17,6 +18,7 @@ import java.util.List;
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.util.Utility;
|
import us.shandian.giga.util.Utility;
|
||||||
|
@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility;
|
||||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
|
|
||||||
public class DownloadMission extends Mission {
|
public class DownloadMission extends Mission {
|
||||||
private static final long serialVersionUID = 3L;// last bump: 8 november 2018
|
private static final long serialVersionUID = 4L;// last bump: 27 march 2019
|
||||||
|
|
||||||
static final int BUFFER_SIZE = 64 * 1024;
|
static final int BUFFER_SIZE = 64 * 1024;
|
||||||
final static int BLOCK_SIZE = 512 * 1024;
|
final static int BLOCK_SIZE = 512 * 1024;
|
||||||
|
@ -43,6 +45,7 @@ public class DownloadMission extends Mission {
|
||||||
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
|
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
|
||||||
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
|
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
|
||||||
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||||
|
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||||
|
|
||||||
|
@ -71,16 +74,6 @@ public class DownloadMission extends Mission {
|
||||||
*/
|
*/
|
||||||
public long[] offsets;
|
public long[] offsets;
|
||||||
|
|
||||||
/**
|
|
||||||
* The post-processing algorithm arguments
|
|
||||||
*/
|
|
||||||
public String[] postprocessingArgs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The post-processing algorithm name
|
|
||||||
*/
|
|
||||||
public String postprocessingName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the post-processing state:
|
* Indicates if the post-processing state:
|
||||||
* 0: ready
|
* 0: ready
|
||||||
|
@ -88,12 +81,12 @@ public class DownloadMission extends Mission {
|
||||||
* 2: completed
|
* 2: completed
|
||||||
* 3: hold
|
* 3: hold
|
||||||
*/
|
*/
|
||||||
public volatile int postprocessingState;
|
public volatile int psState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if the post-processing algorithm works on the same file
|
* the post-processing algorithm instance
|
||||||
*/
|
*/
|
||||||
public boolean postprocessingThis;
|
public transient Postprocessing psAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
|
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
|
||||||
|
@ -138,36 +131,23 @@ public class DownloadMission extends Mission {
|
||||||
public transient volatile Thread[] threads = new Thread[0];
|
public transient volatile Thread[] threads = new Thread[0];
|
||||||
private transient Thread init = null;
|
private transient Thread init = null;
|
||||||
|
|
||||||
|
|
||||||
protected DownloadMission() {
|
protected DownloadMission() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadMission(String url, String name, String location, char kind) {
|
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||||
this(new String[]{url}, name, location, kind, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) {
|
|
||||||
if (name == null) throw new NullPointerException("name is null");
|
|
||||||
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
|
|
||||||
if (urls == null) throw new NullPointerException("urls is null");
|
if (urls == null) throw new NullPointerException("urls is null");
|
||||||
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
|
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
|
||||||
if (location == null) throw new NullPointerException("location is null");
|
|
||||||
if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
|
|
||||||
this.urls = urls;
|
this.urls = urls;
|
||||||
this.name = name;
|
|
||||||
this.location = location;
|
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
this.offsets = new long[urls.length];
|
this.offsets = new long[urls.length];
|
||||||
this.enqueued = true;
|
this.enqueued = true;
|
||||||
this.maxRetry = 3;
|
this.maxRetry = 3;
|
||||||
|
this.storage = storage;
|
||||||
|
|
||||||
if (postprocessingName != null) {
|
if (psInstance != null) {
|
||||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
|
this.psAlgorithm = psInstance;
|
||||||
this.postprocessingThis = algorithm.worksOnSameFile;
|
this.offsets[0] = psInstance.recommendedReserve;
|
||||||
this.offsets[0] = algorithm.recommendedReserve;
|
|
||||||
this.postprocessingName = postprocessingName;
|
|
||||||
this.postprocessingArgs = postprocessingArgs;
|
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG && urls.length > 1) {
|
if (DEBUG && urls.length > 1) {
|
||||||
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
||||||
|
@ -359,22 +339,12 @@ public class DownloadMission extends Mission {
|
||||||
Log.e(TAG, "notifyError() code = " + code, err);
|
Log.e(TAG, "notifyError() code = " + code, err);
|
||||||
|
|
||||||
if (err instanceof IOException) {
|
if (err instanceof IOException) {
|
||||||
if (err.getMessage().contains("Permission denied")) {
|
if (storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||||
code = ERROR_PERMISSION_DENIED;
|
code = ERROR_PERMISSION_DENIED;
|
||||||
err = null;
|
err = null;
|
||||||
} else if (err.getMessage().contains("write failed: ENOSPC")) {
|
} else if (err.getMessage().contains("ENOSPC")) {
|
||||||
code = ERROR_INSUFFICIENT_STORAGE;
|
code = ERROR_INSUFFICIENT_STORAGE;
|
||||||
err = null;
|
err = null;
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
File storage = new File(location);
|
|
||||||
if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) {
|
|
||||||
code = ERROR_INSUFFICIENT_STORAGE;
|
|
||||||
err = null;
|
|
||||||
}
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
// is a permission error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,11 +403,11 @@ public class DownloadMission extends Mission {
|
||||||
action = "Failed";
|
action = "Failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
|
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||||
|
|
||||||
synchronized (blockState) {
|
synchronized (blockState) {
|
||||||
// don't return without fully write the current state
|
// don't return without fully write the current state
|
||||||
postprocessingState = state;
|
psState = state;
|
||||||
Utility.writeToFile(metadata, DownloadMission.this);
|
Utility.writeToFile(metadata, DownloadMission.this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,7 +426,7 @@ public class DownloadMission extends Mission {
|
||||||
running = true;
|
running = true;
|
||||||
errCode = ERROR_NOTHING;
|
errCode = ERROR_NOTHING;
|
||||||
|
|
||||||
if (current >= urls.length && postprocessingName != null) {
|
if (current >= urls.length && psAlgorithm != null) {
|
||||||
runAsync(1, () -> {
|
runAsync(1, () -> {
|
||||||
if (doPostprocessing()) {
|
if (doPostprocessing()) {
|
||||||
running = false;
|
running = false;
|
||||||
|
@ -593,7 +563,7 @@ public class DownloadMission extends Mission {
|
||||||
* @return true, otherwise, false
|
* @return true, otherwise, false
|
||||||
*/
|
*/
|
||||||
public boolean isFinished() {
|
public boolean isFinished() {
|
||||||
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
|
return current >= urls.length && (psAlgorithm == null || psState == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -602,7 +572,13 @@ public class DownloadMission extends Mission {
|
||||||
* @return {@code true} if this mission is unrecoverable
|
* @return {@code true} if this mission is unrecoverable
|
||||||
*/
|
*/
|
||||||
public boolean isPsFailed() {
|
public boolean isPsFailed() {
|
||||||
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
|
switch (errCode) {
|
||||||
|
case ERROR_POSTPROCESSING:
|
||||||
|
case ERROR_POSTPROCESSING_STOPPED:
|
||||||
|
return psAlgorithm.worksOnSameFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -611,7 +587,7 @@ public class DownloadMission extends Mission {
|
||||||
* @return true, otherwise, false
|
* @return true, otherwise, false
|
||||||
*/
|
*/
|
||||||
public boolean isPsRunning() {
|
public boolean isPsRunning() {
|
||||||
return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3);
|
return psAlgorithm != null && (psState == 1 || psState == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -625,7 +601,7 @@ public class DownloadMission extends Mission {
|
||||||
|
|
||||||
public long getLength() {
|
public long getLength() {
|
||||||
long calculated;
|
long calculated;
|
||||||
if (postprocessingState == 1 || postprocessingState == 3) {
|
if (psState == 1 || psState == 3) {
|
||||||
calculated = length;
|
calculated = length;
|
||||||
} else {
|
} else {
|
||||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||||
|
@ -652,38 +628,60 @@ public class DownloadMission extends Mission {
|
||||||
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
|
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
|
||||||
*/
|
*/
|
||||||
public void psContinue(boolean recover) {
|
public void psContinue(boolean recover) {
|
||||||
postprocessingState = 1;
|
psState = 1;
|
||||||
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
|
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
|
||||||
threads[0].interrupt();
|
threads[0].interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* changes the StoredFileHelper for another and saves the changes to the metadata file
|
||||||
|
*
|
||||||
|
* @param newStorage the new StoredFileHelper instance to use
|
||||||
|
*/
|
||||||
|
public void changeStorage(@NonNull StoredFileHelper newStorage) {
|
||||||
|
storage = newStorage;
|
||||||
|
// commit changes on the metadata file
|
||||||
|
runAsync(-2, this::writeThisToFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whatever the backed storage is invalid
|
||||||
|
*
|
||||||
|
* @return {@code true}, if storage is invalid and cannot be used
|
||||||
|
*/
|
||||||
|
public boolean hasInvalidStorage() {
|
||||||
|
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whatever is possible to start the mission
|
||||||
|
*
|
||||||
|
* @return {@code true} is this mission is "sane", otherwise, {@code false}
|
||||||
|
*/
|
||||||
|
public boolean canDownload() {
|
||||||
|
return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean doPostprocessing() {
|
private boolean doPostprocessing() {
|
||||||
if (postprocessingName == null || postprocessingState == 2) return true;
|
if (psAlgorithm == null || psState == 2) return true;
|
||||||
|
|
||||||
notifyPostProcessing(1);
|
notifyPostProcessing(1);
|
||||||
notifyProgress(0);
|
notifyProgress(0);
|
||||||
|
|
||||||
if (DEBUG)
|
if (DEBUG)
|
||||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||||
|
psAlgorithm.getClass().getSimpleName() +
|
||||||
|
" filename = " + storage.getName()
|
||||||
|
);
|
||||||
|
|
||||||
threads = new Thread[]{Thread.currentThread()};
|
threads = new Thread[]{Thread.currentThread()};
|
||||||
|
|
||||||
Exception exception = null;
|
Exception exception = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Postprocessing
|
psAlgorithm.run(this);
|
||||||
.getAlgorithm(postprocessingName, this)
|
|
||||||
.run();
|
|
||||||
} catch (Exception err) {
|
} catch (Exception err) {
|
||||||
StringBuilder args = new StringBuilder(" ");
|
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||||
if (postprocessingArgs != null) {
|
|
||||||
for (String arg : postprocessingArgs) {
|
|
||||||
args.append(", ");
|
|
||||||
args.append(arg);
|
|
||||||
}
|
|
||||||
args.delete(0, 1);
|
|
||||||
}
|
|
||||||
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
|
||||||
|
|
||||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||||
|
|
||||||
|
@ -733,7 +731,7 @@ public class DownloadMission extends Mission {
|
||||||
// >=1: any download thread
|
// >=1: any download thread
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
who.setName(String.format("%s[%s] %s", TAG, id, name));
|
who.setName(String.format("%s[%s] %s", TAG, id, storage.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
who.start();
|
who.start();
|
||||||
|
|
|
@ -2,9 +2,10 @@ package us.shandian.giga.get;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
|
|
||||||
|
@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread {
|
||||||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||||
}
|
}
|
||||||
|
|
||||||
RandomAccessFile f;
|
SharpStream f;
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
f = mMission.storage.getStream();
|
||||||
} catch (FileNotFoundException e) {
|
} catch (IOException e) {
|
||||||
mMission.notifyError(e);// this never should happen
|
mMission.notifyError(e);// this never should happen
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import android.annotation.SuppressLint;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
|
|
||||||
|
|
||||||
import us.shandian.giga.util.Utility;
|
import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
|
@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread {
|
||||||
private static final String TAG = "DownloadRunnableFallback";
|
private static final String TAG = "DownloadRunnableFallback";
|
||||||
|
|
||||||
private final DownloadMission mMission;
|
private final DownloadMission mMission;
|
||||||
private final int mId = 1;
|
|
||||||
|
|
||||||
private int mRetryCount = 0;
|
private int mRetryCount = 0;
|
||||||
private InputStream mIs;
|
private InputStream mIs;
|
||||||
private RandomAccessFile mF;
|
private SharpStream mF;
|
||||||
private HttpURLConnection mConn;
|
private HttpURLConnection mConn;
|
||||||
|
|
||||||
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
||||||
|
@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (mF != null) mF.close();
|
||||||
if (mF != null) mF.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread {
|
||||||
try {
|
try {
|
||||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||||
|
|
||||||
|
int mId = 1;
|
||||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||||
mMission.establishConnection(mId, mConn);
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
|
@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread {
|
||||||
if (!mMission.unknownLength)
|
if (!mMission.unknownLength)
|
||||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||||
|
|
||||||
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
mF = mMission.storage.getStream();
|
||||||
mF.seek(mMission.offsets[mMission.current] + start);
|
mF.seek(mMission.offsets[mMission.current] + start);
|
||||||
|
|
||||||
mIs = mConn.getInputStream();
|
mIs = mConn.getInputStream();
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
public class FinishedMission extends Mission {
|
public class FinishedMission extends Mission {
|
||||||
|
|
||||||
public FinishedMission() {
|
public FinishedMission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public FinishedMission(DownloadMission mission) {
|
public FinishedMission(@NonNull DownloadMission mission) {
|
||||||
source = mission.source;
|
source = mission.source;
|
||||||
length = mission.length;// ¿or mission.done?
|
length = mission.length;// ¿or mission.done?
|
||||||
timestamp = mission.timestamp;
|
timestamp = mission.timestamp;
|
||||||
name = mission.name;
|
|
||||||
location = mission.location;
|
|
||||||
kind = mission.kind;
|
kind = mission.kind;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
import java.io.File;
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
|
|
||||||
public abstract class Mission implements Serializable {
|
public abstract class Mission implements Serializable {
|
||||||
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
|
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source url of the resource
|
* Source url of the resource
|
||||||
|
@ -23,28 +26,23 @@ public abstract class Mission implements Serializable {
|
||||||
*/
|
*/
|
||||||
public long timestamp;
|
public long timestamp;
|
||||||
|
|
||||||
/**
|
|
||||||
* The filename
|
|
||||||
*/
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The directory to store the download
|
|
||||||
*/
|
|
||||||
public String location;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pre-defined content type
|
* pre-defined content type
|
||||||
*/
|
*/
|
||||||
public char kind;
|
public char kind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The downloaded file
|
||||||
|
*/
|
||||||
|
public StoredFileHelper storage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the target file on the storage
|
* get the target file on the storage
|
||||||
*
|
*
|
||||||
* @return File object
|
* @return File object
|
||||||
*/
|
*/
|
||||||
public File getDownloadedFile() {
|
public Uri getDownloadedFileUri() {
|
||||||
return new File(location, name);
|
return storage.getUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,8 +51,8 @@ public abstract class Mission implements Serializable {
|
||||||
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
|
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
|
||||||
*/
|
*/
|
||||||
public boolean delete() {
|
public boolean delete() {
|
||||||
deleted = true;
|
if (storage != null) return storage.delete();
|
||||||
return getDownloadedFile().delete();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,10 +60,11 @@ public abstract class Mission implements Serializable {
|
||||||
*/
|
*/
|
||||||
public transient boolean deleted = false;
|
public transient boolean deleted = false;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
calendar.setTimeInMillis(timestamp);
|
calendar.setTimeInMillis(timestamp);
|
||||||
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
|
return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
package us.shandian.giga.get.sqlite;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
import us.shandian.giga.get.FinishedMission;
|
|
||||||
import us.shandian.giga.get.Mission;
|
|
||||||
|
|
||||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION;
|
|
||||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME;
|
|
||||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME;
|
|
||||||
|
|
||||||
public class DownloadDataSource {
|
|
||||||
|
|
||||||
private static final String TAG = "DownloadDataSource";
|
|
||||||
private final DownloadMissionHelper downloadMissionHelper;
|
|
||||||
|
|
||||||
public DownloadDataSource(Context context) {
|
|
||||||
downloadMissionHelper = new DownloadMissionHelper(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayList<FinishedMission> loadFinishedMissions() {
|
|
||||||
SQLiteDatabase database = downloadMissionHelper.getReadableDatabase();
|
|
||||||
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
|
|
||||||
null, null, null, DownloadMissionHelper.KEY_TIMESTAMP);
|
|
||||||
|
|
||||||
int count = cursor.getCount();
|
|
||||||
if (count == 0) return new ArrayList<>(1);
|
|
||||||
|
|
||||||
ArrayList<FinishedMission> result = new ArrayList<>(count);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
result.add(DownloadMissionHelper.getMissionFromCursor(cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addMission(DownloadMission downloadMission) {
|
|
||||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
|
||||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
|
||||||
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
|
|
||||||
database.insert(MISSIONS_TABLE_NAME, null, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteMission(Mission downloadMission) {
|
|
||||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
|
||||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
|
||||||
database.delete(MISSIONS_TABLE_NAME,
|
|
||||||
KEY_LOCATION + " = ? AND " +
|
|
||||||
KEY_NAME + " = ?",
|
|
||||||
new String[]{downloadMission.location, downloadMission.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateMission(DownloadMission downloadMission) {
|
|
||||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
|
||||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
|
||||||
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
|
|
||||||
String whereClause = KEY_LOCATION + " = ? AND " +
|
|
||||||
KEY_NAME + " = ?";
|
|
||||||
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
|
|
||||||
whereClause, new String[]{downloadMission.location, downloadMission.name});
|
|
||||||
if (rowsAffected != 1) {
|
|
||||||
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
package us.shandian.giga.get.sqlite;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
import us.shandian.giga.get.FinishedMission;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
|
|
||||||
*/
|
|
||||||
public class DownloadMissionHelper extends SQLiteOpenHelper {
|
|
||||||
private final String TAG = "DownloadMissionHelper";
|
|
||||||
|
|
||||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
|
||||||
private static final String DATABASE_NAME = "downloads.db";
|
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The table name of download missions
|
|
||||||
*/
|
|
||||||
static final String MISSIONS_TABLE_NAME = "download_missions";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key to the directory location of the mission
|
|
||||||
*/
|
|
||||||
static final String KEY_LOCATION = "location";
|
|
||||||
/**
|
|
||||||
* The key to the urls of a mission
|
|
||||||
*/
|
|
||||||
static final String KEY_SOURCE_URL = "url";
|
|
||||||
/**
|
|
||||||
* The key to the name of a mission
|
|
||||||
*/
|
|
||||||
static final String KEY_NAME = "name";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key to the done.
|
|
||||||
*/
|
|
||||||
static final String KEY_DONE = "bytes_downloaded";
|
|
||||||
|
|
||||||
static final String KEY_TIMESTAMP = "timestamp";
|
|
||||||
|
|
||||||
static final String KEY_KIND = "kind";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The statement to create the table
|
|
||||||
*/
|
|
||||||
private static final String MISSIONS_CREATE_TABLE =
|
|
||||||
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
|
|
||||||
KEY_LOCATION + " TEXT NOT NULL, " +
|
|
||||||
KEY_NAME + " TEXT NOT NULL, " +
|
|
||||||
KEY_SOURCE_URL + " TEXT NOT NULL, " +
|
|
||||||
KEY_DONE + " INTEGER NOT NULL, " +
|
|
||||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
|
||||||
KEY_KIND + " TEXT NOT NULL, " +
|
|
||||||
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
|
|
||||||
|
|
||||||
public DownloadMissionHelper(Context context) {
|
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
if (oldVersion == 2) {
|
|
||||||
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all values of the download mission as ContentValues.
|
|
||||||
*
|
|
||||||
* @param downloadMission the download mission
|
|
||||||
* @return the content values
|
|
||||||
*/
|
|
||||||
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(KEY_SOURCE_URL, downloadMission.source);
|
|
||||||
values.put(KEY_LOCATION, downloadMission.location);
|
|
||||||
values.put(KEY_NAME, downloadMission.name);
|
|
||||||
values.put(KEY_DONE, downloadMission.done);
|
|
||||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
|
||||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FinishedMission getMissionFromCursor(Cursor cursor) {
|
|
||||||
if (cursor == null) throw new NullPointerException("cursor is null");
|
|
||||||
|
|
||||||
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
|
|
||||||
if (kind == null || kind.isEmpty()) kind = "?";
|
|
||||||
|
|
||||||
FinishedMission mission = new FinishedMission();
|
|
||||||
mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
|
|
||||||
mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
|
|
||||||
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));;
|
|
||||||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
|
||||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
|
||||||
mission.kind = kind.charAt(0);
|
|
||||||
|
|
||||||
return mission;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
package us.shandian.giga.get.sqlite;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import us.shandian.giga.get.DownloadMission;
|
||||||
|
import us.shandian.giga.get.FinishedMission;
|
||||||
|
import us.shandian.giga.get.Mission;
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s
|
||||||
|
*/
|
||||||
|
public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||||
|
private static final String DATABASE_NAME = "downloads.db";
|
||||||
|
|
||||||
|
private static final int DATABASE_VERSION = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table name of download missions (old)
|
||||||
|
*/
|
||||||
|
private static final String MISSIONS_TABLE_NAME_v2 = "download_missions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table name of download missions
|
||||||
|
*/
|
||||||
|
private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key to the urls of a mission
|
||||||
|
*/
|
||||||
|
private static final String KEY_SOURCE = "url";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key to the done.
|
||||||
|
*/
|
||||||
|
private static final String KEY_DONE = "bytes_downloaded";
|
||||||
|
|
||||||
|
private static final String KEY_TIMESTAMP = "timestamp";
|
||||||
|
|
||||||
|
private static final String KEY_KIND = "kind";
|
||||||
|
|
||||||
|
private static final String KEY_PATH = "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The statement to create the table
|
||||||
|
*/
|
||||||
|
private static final String MISSIONS_CREATE_TABLE =
|
||||||
|
"CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" +
|
||||||
|
KEY_PATH + " TEXT NOT NULL, " +
|
||||||
|
KEY_SOURCE + " TEXT NOT NULL, " +
|
||||||
|
KEY_DONE + " INTEGER NOT NULL, " +
|
||||||
|
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||||
|
KEY_KIND + " TEXT NOT NULL, " +
|
||||||
|
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
||||||
|
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public FinishedMissionStore(Context context) {
|
||||||
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
if (oldVersion == 2) {
|
||||||
|
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;");
|
||||||
|
oldVersion++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion == 3) {
|
||||||
|
final String KEY_LOCATION = "location";
|
||||||
|
final String KEY_NAME = "name";
|
||||||
|
|
||||||
|
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||||
|
|
||||||
|
Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null,
|
||||||
|
null, null, null, KEY_TIMESTAMP);
|
||||||
|
|
||||||
|
int count = cursor.getCount();
|
||||||
|
if (count > 0) {
|
||||||
|
db.beginTransaction();
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
|
||||||
|
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
|
||||||
|
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
|
||||||
|
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
|
||||||
|
values.put(KEY_PATH, Uri.fromFile(
|
||||||
|
new File(
|
||||||
|
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
|
||||||
|
cursor.getString(cursor.getColumnIndex(KEY_NAME))
|
||||||
|
)
|
||||||
|
).toString());
|
||||||
|
|
||||||
|
db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close();
|
||||||
|
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all values of the download mission as ContentValues.
|
||||||
|
*
|
||||||
|
* @param downloadMission the download mission
|
||||||
|
* @return the content values
|
||||||
|
*/
|
||||||
|
private ContentValues getValuesOfMission(@NonNull Mission downloadMission) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(KEY_SOURCE, downloadMission.source);
|
||||||
|
values.put(KEY_PATH, downloadMission.storage.getUri().toString());
|
||||||
|
values.put(KEY_DONE, downloadMission.length);
|
||||||
|
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||||
|
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FinishedMission getMissionFromCursor(Cursor cursor) {
|
||||||
|
if (cursor == null) throw new NullPointerException("cursor is null");
|
||||||
|
|
||||||
|
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
|
||||||
|
if (kind == null || kind.isEmpty()) kind = "?";
|
||||||
|
|
||||||
|
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
|
||||||
|
|
||||||
|
FinishedMission mission = new FinishedMission();
|
||||||
|
|
||||||
|
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE));
|
||||||
|
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||||
|
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||||
|
mission.kind = kind.charAt(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mission.storage = new StoredFileHelper(context, Uri.parse(path), "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
|
||||||
|
mission.storage = new StoredFileHelper(path, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////
|
||||||
|
// Data source methods
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
public ArrayList<FinishedMission> loadFinishedMissions() {
|
||||||
|
SQLiteDatabase database = getReadableDatabase();
|
||||||
|
Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null,
|
||||||
|
null, null, null, KEY_TIMESTAMP + " DESC");
|
||||||
|
|
||||||
|
int count = cursor.getCount();
|
||||||
|
if (count == 0) return new ArrayList<>(1);
|
||||||
|
|
||||||
|
ArrayList<FinishedMission> result = new ArrayList<>(count);
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
result.add(getMissionFromCursor(cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFinishedMission(DownloadMission downloadMission) {
|
||||||
|
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||||
|
SQLiteDatabase database = getWritableDatabase();
|
||||||
|
ContentValues values = getValuesOfMission(downloadMission);
|
||||||
|
database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMission(Mission mission) {
|
||||||
|
if (mission == null) throw new NullPointerException("mission is null");
|
||||||
|
String path = mission.getDownloadedFileUri().toString();
|
||||||
|
|
||||||
|
SQLiteDatabase database = getWritableDatabase();
|
||||||
|
|
||||||
|
if (mission instanceof FinishedMission)
|
||||||
|
database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path});
|
||||||
|
else
|
||||||
|
throw new UnsupportedOperationException("DownloadMission");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMission(Mission mission) {
|
||||||
|
if (mission == null) throw new NullPointerException("mission is null");
|
||||||
|
SQLiteDatabase database = getWritableDatabase();
|
||||||
|
ContentValues values = getValuesOfMission(mission);
|
||||||
|
String path = mission.getDownloadedFileUri().toString();
|
||||||
|
|
||||||
|
int rowsAffected;
|
||||||
|
|
||||||
|
if (mission instanceof FinishedMission)
|
||||||
|
rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path});
|
||||||
|
else
|
||||||
|
throw new UnsupportedOperationException("DownloadMission");
|
||||||
|
|
||||||
|
if (rowsAffected != 1) {
|
||||||
|
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,150 +1,148 @@
|
||||||
package us.shandian.giga.postprocessing.io;
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.IOException;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
public class ChunkFileInputStream extends SharpStream {
|
||||||
|
|
||||||
public class ChunkFileInputStream extends SharpStream {
|
private SharpStream source;
|
||||||
|
private final long offset;
|
||||||
private RandomAccessFile source;
|
private final long length;
|
||||||
private final long offset;
|
private long position;
|
||||||
private final long length;
|
|
||||||
private long position;
|
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||||
|
this(target, start, target.length());
|
||||||
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
|
}
|
||||||
source = new RandomAccessFile(file, mode);
|
|
||||||
offset = start;
|
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||||
length = end - start;
|
source = target;
|
||||||
position = 0;
|
offset = start;
|
||||||
|
length = end - start;
|
||||||
if (length < 1) {
|
position = 0;
|
||||||
source.close();
|
|
||||||
throw new IOException("The chunk is empty or invalid");
|
if (length < 1) {
|
||||||
}
|
source.close();
|
||||||
if (source.length() < end) {
|
throw new IOException("The chunk is empty or invalid");
|
||||||
try {
|
}
|
||||||
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
if (source.length() < end) {
|
||||||
} finally {
|
try {
|
||||||
source.close();
|
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
||||||
}
|
} finally {
|
||||||
}
|
source.close();
|
||||||
|
}
|
||||||
source.seek(offset);
|
}
|
||||||
}
|
|
||||||
|
source.seek(offset);
|
||||||
/**
|
}
|
||||||
* Get absolute position on file
|
|
||||||
*
|
/**
|
||||||
* @return the position
|
* Get absolute position on file
|
||||||
*/
|
*
|
||||||
public long getFilePointer() {
|
* @return the position
|
||||||
return offset + position;
|
*/
|
||||||
}
|
public long getFilePointer() {
|
||||||
|
return offset + position;
|
||||||
@Override
|
}
|
||||||
public int read() throws IOException {
|
|
||||||
if ((position + 1) > length) {
|
@Override
|
||||||
return 0;
|
public int read() throws IOException {
|
||||||
}
|
if ((position + 1) > length) {
|
||||||
|
return 0;
|
||||||
int res = source.read();
|
}
|
||||||
if (res >= 0) {
|
|
||||||
position++;
|
int res = source.read();
|
||||||
}
|
if (res >= 0) {
|
||||||
|
position++;
|
||||||
return res;
|
}
|
||||||
}
|
|
||||||
|
return res;
|
||||||
@Override
|
}
|
||||||
public int read(byte b[]) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
@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) {
|
@Override
|
||||||
len = (int) (length - position);
|
public int read(byte b[], int off, int len) throws IOException {
|
||||||
}
|
if ((position + len) > length) {
|
||||||
if (len == 0) {
|
len = (int) (length - position);
|
||||||
return 0;
|
}
|
||||||
}
|
if (len == 0) {
|
||||||
|
return 0;
|
||||||
int res = source.read(b, off, len);
|
}
|
||||||
position += res;
|
|
||||||
|
int res = source.read(b, off, len);
|
||||||
return res;
|
position += res;
|
||||||
}
|
|
||||||
|
return res;
|
||||||
@Override
|
}
|
||||||
public long skip(long pos) throws IOException {
|
|
||||||
pos = Math.min(pos + position, length);
|
@Override
|
||||||
|
public long skip(long pos) throws IOException {
|
||||||
if (pos == 0) {
|
pos = Math.min(pos + position, length);
|
||||||
return 0;
|
|
||||||
}
|
if (pos == 0) {
|
||||||
|
return 0;
|
||||||
source.seek(offset + pos);
|
}
|
||||||
|
|
||||||
long oldPos = position;
|
source.seek(offset + pos);
|
||||||
position = pos;
|
|
||||||
|
long oldPos = position;
|
||||||
return pos - oldPos;
|
position = pos;
|
||||||
}
|
|
||||||
|
return pos - oldPos;
|
||||||
@Override
|
}
|
||||||
public long available() {
|
|
||||||
return (int) (length - position);
|
@Override
|
||||||
}
|
public long available() {
|
||||||
|
return (int) (length - position);
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
}
|
||||||
@Override
|
|
||||||
public void dispose() {
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
try {
|
@Override
|
||||||
source.close();
|
public void close() {
|
||||||
} catch (IOException err) {
|
source.close();
|
||||||
} finally {
|
source = null;
|
||||||
source = null;
|
}
|
||||||
}
|
|
||||||
}
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
@Override
|
return source == null;
|
||||||
public boolean isDisposed() {
|
}
|
||||||
return source == null;
|
|
||||||
}
|
@Override
|
||||||
|
public void rewind() throws IOException {
|
||||||
@Override
|
position = 0;
|
||||||
public void rewind() throws IOException {
|
source.seek(offset);
|
||||||
position = 0;
|
}
|
||||||
source.seek(offset);
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canRewind() {
|
||||||
@Override
|
return true;
|
||||||
public boolean canRewind() {
|
}
|
||||||
return true;
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canRead() {
|
||||||
@Override
|
return true;
|
||||||
public boolean canRead() {
|
}
|
||||||
return true;
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canWrite() {
|
||||||
@Override
|
return false;
|
||||||
public boolean canWrite() {
|
}
|
||||||
return false;
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte value) {
|
||||||
@Override
|
}
|
||||||
public void write(byte value) {
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte[] buffer) {
|
||||||
@Override
|
}
|
||||||
public void write(byte[] buffer) {
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int count) {
|
||||||
@Override
|
}
|
||||||
public void write(byte[] buffer, int offset, int count) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package us.shandian.giga.postprocessing.io;
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
|
|
||||||
public class CircularFileWriter extends SharpStream {
|
public class CircularFileWriter extends SharpStream {
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
private BufferedFile out;
|
private BufferedFile out;
|
||||||
private BufferedFile aux;
|
private BufferedFile aux;
|
||||||
|
|
||||||
public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException {
|
public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException {
|
||||||
if (checker == null) {
|
if (checker == null) {
|
||||||
throw new NullPointerException("checker is null");
|
throw new NullPointerException("checker is null");
|
||||||
}
|
}
|
||||||
|
@ -38,7 +37,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
aux = new BufferedFile(temp);
|
aux = new BufferedFile(temp);
|
||||||
out = new BufferedFile(source);
|
out = new BufferedFile(target);
|
||||||
|
|
||||||
callback = checker;
|
callback = checker;
|
||||||
|
|
||||||
|
@ -105,7 +104,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
out.target.setLength(length);
|
out.target.setLength(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose();
|
close();
|
||||||
|
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
@ -114,13 +113,13 @@ public class CircularFileWriter extends SharpStream {
|
||||||
* Close the file without flushing any buffer
|
* Close the file without flushing any buffer
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void close() {
|
||||||
if (out != null) {
|
if (out != null) {
|
||||||
out.dispose();
|
out.close();
|
||||||
out = null;
|
out = null;
|
||||||
}
|
}
|
||||||
if (aux != null) {
|
if (aux != null) {
|
||||||
aux.dispose();
|
aux.close();
|
||||||
aux = null;
|
aux = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +255,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isDisposed() {
|
public boolean isClosed() {
|
||||||
return out == null;
|
return out == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,30 +338,29 @@ public class CircularFileWriter extends SharpStream {
|
||||||
|
|
||||||
class BufferedFile {
|
class BufferedFile {
|
||||||
|
|
||||||
protected final RandomAccessFile target;
|
protected final SharpStream target;
|
||||||
|
|
||||||
private long offset;
|
private long offset;
|
||||||
protected long length;
|
protected long length;
|
||||||
|
|
||||||
private byte[] queue;
|
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||||
private int queueSize;
|
private int queueSize;
|
||||||
|
|
||||||
BufferedFile(File file) throws FileNotFoundException {
|
BufferedFile(File file) throws FileNotFoundException {
|
||||||
queue = new byte[QUEUE_BUFFER_SIZE];
|
this.target = new FileStream(file);
|
||||||
target = new RandomAccessFile(file, "rw");
|
}
|
||||||
|
|
||||||
|
BufferedFile(SharpStream target) {
|
||||||
|
this.target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected long getOffset() {
|
protected long getOffset() {
|
||||||
return offset + queueSize;// absolute offset in the file
|
return offset + queueSize;// absolute offset in the file
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void dispose() {
|
protected void close() {
|
||||||
try {
|
queue = null;
|
||||||
queue = null;
|
target.close();
|
||||||
target.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void write(byte b[], int off, int len) throws IOException {
|
protected void write(byte b[], int off, int len) throws IOException {
|
||||||
|
@ -384,7 +382,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void flush() throws IOException {
|
void flush() throws IOException {
|
||||||
writeProof(queue, queueSize);
|
writeProof(queue, queueSize);
|
||||||
offset += queueSize;
|
offset += queueSize;
|
||||||
queueSize = 0;
|
queueSize = 0;
|
||||||
|
@ -404,7 +402,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
return queue.length - queueSize;
|
return queue.length - queueSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void reset() throws IOException {
|
void reset() throws IOException {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
length = 0;
|
length = 0;
|
||||||
target.seek(0);
|
target.seek(0);
|
||||||
|
@ -415,7 +413,7 @@ public class CircularFileWriter extends SharpStream {
|
||||||
target.seek(absoluteOffset);
|
target.seek(absoluteOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void writeProof(byte[] buffer, int length) throws IOException {
|
void writeProof(byte[] buffer, int length) throws IOException {
|
||||||
if (onWriteError == null) {
|
if (onWriteError == null) {
|
||||||
target.write(buffer, 0, length);
|
target.write(buffer, 0, length);
|
||||||
return;
|
return;
|
||||||
|
@ -436,14 +434,8 @@ public class CircularFileWriter extends SharpStream {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
String absOffset;
|
|
||||||
String absLength;
|
String absLength;
|
||||||
|
|
||||||
try {
|
|
||||||
absOffset = Long.toString(target.getFilePointer());
|
|
||||||
} catch (IOException e) {
|
|
||||||
absOffset = "[" + e.getLocalizedMessage() + "]";
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
absLength = Long.toString(target.length());
|
absLength = Long.toString(target.length());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -451,8 +443,8 @@ public class CircularFileWriter extends SharpStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
return String.format(
|
return String.format(
|
||||||
"offset=%s length=%s queue=%s absOffset=%s absLength=%s",
|
"offset=%s length=%s queue=%s absLength=%s",
|
||||||
offset, length, queueSize, absOffset, absLength
|
offset, length, queueSize, absLength
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
131
app/src/main/java/us/shandian/giga/io/FileStream.java
Normal file
131
app/src/main/java/us/shandian/giga/io/FileStream.java
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kapodamy
|
||||||
|
*/
|
||||||
|
public class FileStream extends SharpStream {
|
||||||
|
|
||||||
|
public RandomAccessFile source;
|
||||||
|
|
||||||
|
public FileStream(@NonNull File target) throws FileNotFoundException {
|
||||||
|
this.source = new RandomAccessFile(target, "rw");
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileStream(@NonNull String path) throws FileNotFoundException {
|
||||||
|
this.source = new RandomAccessFile(path, "rw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return source.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte b[]) throws IOException {
|
||||||
|
return source.read(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
return source.skipBytes((int) pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long available() {
|
||||||
|
try {
|
||||||
|
return source.length() - source.getFilePointer();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (source == null) return;
|
||||||
|
try {
|
||||||
|
source.close();
|
||||||
|
} catch (IOException err) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
source = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return source == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rewind() throws IOException {
|
||||||
|
source.seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRewind() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canWrite() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canSeek() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canSetLength() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 setLength(long length) throws IOException {
|
||||||
|
source.setLength(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seek(long offset) throws IOException {
|
||||||
|
source.seek(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long length() throws IOException {
|
||||||
|
return source.length();
|
||||||
|
}
|
||||||
|
}
|
140
app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
Normal file
140
app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
public class FileStreamSAF extends SharpStream {
|
||||||
|
|
||||||
|
private final FileInputStream in;
|
||||||
|
private final FileOutputStream out;
|
||||||
|
private final FileChannel channel;
|
||||||
|
private final ParcelFileDescriptor file;
|
||||||
|
|
||||||
|
private boolean disposed;
|
||||||
|
|
||||||
|
public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException {
|
||||||
|
// Notes:
|
||||||
|
// the file must exists first
|
||||||
|
// ¡read-write mode must allow seek!
|
||||||
|
// It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices
|
||||||
|
|
||||||
|
file = contentResolver.openFileDescriptor(fileUri, "rw");
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
in = new FileInputStream(file.getFileDescriptor());
|
||||||
|
out = new FileOutputStream(file.getFileDescriptor());
|
||||||
|
channel = out.getChannel();// or use in.getChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return in.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer) throws IOException {
|
||||||
|
return in.read(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
return in.read(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long amount) throws IOException {
|
||||||
|
return in.skip(amount);// ¿or use channel.position(channel.position() + amount)?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long available() {
|
||||||
|
try {
|
||||||
|
return in.available();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;// ¡but not -1!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rewind() throws IOException {
|
||||||
|
seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
disposed = true;
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
channel.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("FileStreamSAF", "close() error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return disposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRewind() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canWrite() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSetLength() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSeek() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte value) throws IOException {
|
||||||
|
out.write(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer) throws IOException {
|
||||||
|
out.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
out.write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLength(long length) throws IOException {
|
||||||
|
channel.truncate(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seek(long offset) throws IOException {
|
||||||
|
channel.position(offset);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,61 +1,61 @@
|
||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
* To change this template file, choose Tools | Templates
|
* To change this template file, choose Tools | Templates
|
||||||
* and open the template in the editor.
|
* and open the template in the editor.
|
||||||
*/
|
*/
|
||||||
package us.shandian.giga.postprocessing.io;
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for the classic {@link java.io.InputStream}
|
* Wrapper for the classic {@link java.io.InputStream}
|
||||||
*
|
*
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
public class SharpInputStream extends InputStream {
|
public class SharpInputStream extends InputStream {
|
||||||
|
|
||||||
private final SharpStream base;
|
private final SharpStream base;
|
||||||
|
|
||||||
public SharpInputStream(SharpStream base) throws IOException {
|
public SharpInputStream(SharpStream base) throws IOException {
|
||||||
if (!base.canRead()) {
|
if (!base.canRead()) {
|
||||||
throw new IOException("The provided stream is not readable");
|
throw new IOException("The provided stream is not readable");
|
||||||
}
|
}
|
||||||
this.base = base;
|
this.base = base;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read() throws IOException {
|
public int read() throws IOException {
|
||||||
return base.read();
|
return base.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(@NonNull byte[] bytes) throws IOException {
|
public int read(@NonNull byte[] bytes) throws IOException {
|
||||||
return base.read(bytes);
|
return base.read(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
||||||
return base.read(bytes, i, i1);
|
return base.read(bytes, i, i1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long skip(long l) throws IOException {
|
public long skip(long l) throws IOException {
|
||||||
return base.skip(l);
|
return base.skip(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int available() {
|
public int available() {
|
||||||
long res = base.available();
|
long res = base.available();
|
||||||
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
|
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
base.dispose();
|
base.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
175
app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
Normal file
175
app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class StoredDirectoryHelper {
|
||||||
|
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||||
|
|
||||||
|
private File ioTree;
|
||||||
|
private DocumentFile docTree;
|
||||||
|
|
||||||
|
private ContentResolver contentResolver;
|
||||||
|
|
||||||
|
private String tag;
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
|
||||||
|
this.contentResolver = context.getContentResolver();
|
||||||
|
this.tag = tag;
|
||||||
|
this.docTree = DocumentFile.fromTreeUri(context, path);
|
||||||
|
|
||||||
|
if (this.docTree == null)
|
||||||
|
throw new IOException("Failed to create the tree from Uri");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public StoredDirectoryHelper(@NonNull String location, String tag) {
|
||||||
|
ioTree = new File(location);
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public StoredFileHelper createFile(String filename, String mime) {
|
||||||
|
StoredFileHelper storage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (docTree == null) {
|
||||||
|
storage = new StoredFileHelper(ioTree, filename, tag);
|
||||||
|
storage.sourceTree = Uri.fromFile(ioTree).toString();
|
||||||
|
} else {
|
||||||
|
storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag);
|
||||||
|
storage.sourceTree = docTree.getUri().toString();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.tag = tag;
|
||||||
|
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredFileHelper createUniqueFile(String filename, String mime) {
|
||||||
|
ArrayList<String> existingNames = new ArrayList<>(50);
|
||||||
|
|
||||||
|
String ext;
|
||||||
|
|
||||||
|
int dotIndex = filename.lastIndexOf('.');
|
||||||
|
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
|
||||||
|
ext = "";
|
||||||
|
} else {
|
||||||
|
ext = filename.substring(dotIndex);
|
||||||
|
filename = filename.substring(0, dotIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name;
|
||||||
|
if (docTree == null) {
|
||||||
|
for (File file : ioTree.listFiles()) {
|
||||||
|
name = file.getName().toLowerCase();
|
||||||
|
if (name.startsWith(filename)) existingNames.add(name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (DocumentFile file : docTree.listFiles()) {
|
||||||
|
name = file.getName();
|
||||||
|
if (name == null) continue;
|
||||||
|
name = name.toLowerCase();
|
||||||
|
if (name.startsWith(filename)) existingNames.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean free = true;
|
||||||
|
String lwFilename = filename.toLowerCase();
|
||||||
|
for (String testName : existingNames) {
|
||||||
|
if (testName.equals(lwFilename)) {
|
||||||
|
free = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (free) return createFile(filename, mime);
|
||||||
|
|
||||||
|
String[] sortedNames = existingNames.toArray(new String[0]);
|
||||||
|
Arrays.sort(sortedNames);
|
||||||
|
|
||||||
|
String newName;
|
||||||
|
int downloadIndex = 0;
|
||||||
|
do {
|
||||||
|
newName = filename + " (" + downloadIndex + ")" + ext;
|
||||||
|
++downloadIndex;
|
||||||
|
if (downloadIndex == 1000) { // Probably an error on our side
|
||||||
|
newName = System.currentTimeMillis() + ext;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (Arrays.binarySearch(sortedNames, newName) >= 0);
|
||||||
|
|
||||||
|
|
||||||
|
return createFile(newName, mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirect() {
|
||||||
|
return docTree == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getUri() {
|
||||||
|
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean exists() {
|
||||||
|
return docTree == null ? ioTree.exists() : docTree.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acquirePermissions() throws IOException {
|
||||||
|
if (docTree == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void revokePermissions() throws IOException {
|
||||||
|
if (docTree == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri findFile(String filename) {
|
||||||
|
if (docTree == null)
|
||||||
|
return Uri.fromFile(new File(ioTree, filename));
|
||||||
|
|
||||||
|
// findFile() method is very slow
|
||||||
|
DocumentFile file = docTree.findFile(filename);
|
||||||
|
|
||||||
|
return file == null ? null : file.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
301
app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
Normal file
301
app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class StoredFileHelper implements Serializable {
|
||||||
|
private static final long serialVersionUID = 0L;
|
||||||
|
public static final String DEFAULT_MIME = "application/octet-stream";
|
||||||
|
|
||||||
|
private transient DocumentFile docFile;
|
||||||
|
private transient DocumentFile docTree;
|
||||||
|
private transient File ioFile;
|
||||||
|
private transient ContentResolver contentResolver;
|
||||||
|
|
||||||
|
protected String source;
|
||||||
|
String sourceTree;
|
||||||
|
|
||||||
|
protected String tag;
|
||||||
|
|
||||||
|
private String srcName;
|
||||||
|
private String srcType;
|
||||||
|
|
||||||
|
public StoredFileHelper(String filename, String mime, String tag) {
|
||||||
|
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
|
||||||
|
|
||||||
|
this.srcName = filename;
|
||||||
|
this.srcType = mime == null ? DEFAULT_MIME : mime;
|
||||||
|
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException {
|
||||||
|
this.docTree = tree;
|
||||||
|
this.contentResolver = contentResolver;
|
||||||
|
|
||||||
|
// this is very slow, because SAF does not allow overwrite
|
||||||
|
DocumentFile res = this.docTree.findFile(filename);
|
||||||
|
|
||||||
|
if (res != null && res.exists() && res.isDirectory()) {
|
||||||
|
if (!res.delete())
|
||||||
|
throw new IOException("Directory with the same name found but cannot delete");
|
||||||
|
res = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == null) {
|
||||||
|
res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename);
|
||||||
|
if (res == null) throw new IOException("Cannot create the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.docFile = res;
|
||||||
|
this.source = res.getUri().toString();
|
||||||
|
this.srcName = getName();
|
||||||
|
this.srcType = getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException {
|
||||||
|
this.source = path.toString();
|
||||||
|
this.tag = tag;
|
||||||
|
|
||||||
|
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) {
|
||||||
|
this.ioFile = new File(URI.create(this.source));
|
||||||
|
} else {
|
||||||
|
DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||||
|
if (file == null)
|
||||||
|
throw new UnsupportedOperationException("Cannot get the file via SAF");
|
||||||
|
|
||||||
|
this.contentResolver = context.getContentResolver();
|
||||||
|
this.docFile = file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.srcName = getName();
|
||||||
|
this.srcType = getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredFileHelper(File location, String filename, String tag) throws IOException {
|
||||||
|
this.ioFile = new File(location, filename);
|
||||||
|
this.tag = tag;
|
||||||
|
|
||||||
|
if (this.ioFile.exists()) {
|
||||||
|
if (!this.ioFile.isFile() && !this.ioFile.delete())
|
||||||
|
throw new IOException("The filename is already in use by non-file entity and cannot overwrite it");
|
||||||
|
} else {
|
||||||
|
if (!this.ioFile.createNewFile())
|
||||||
|
throw new IOException("Cannot create the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.source = Uri.fromFile(this.ioFile).toString();
|
||||||
|
this.srcName = getName();
|
||||||
|
this.srcType = getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
|
||||||
|
if (storage.isInvalid())
|
||||||
|
return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag);
|
||||||
|
|
||||||
|
StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag);
|
||||||
|
|
||||||
|
if (storage.sourceTree != null) {
|
||||||
|
instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree));
|
||||||
|
|
||||||
|
if (instance.docTree == null)
|
||||||
|
throw new IOException("Cannot deserialize the tree, ¿revoked permissions?");
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) {
|
||||||
|
// SAF notes:
|
||||||
|
// ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files
|
||||||
|
// ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict
|
||||||
|
|
||||||
|
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
.setType(mime)
|
||||||
|
.putExtra(Intent.EXTRA_TITLE, filename)
|
||||||
|
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS)
|
||||||
|
.putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks
|
||||||
|
|
||||||
|
who.startActivityForResult(intent, requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharpStream getStream() throws IOException {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
if (docFile == null)
|
||||||
|
return new FileStream(ioFile);
|
||||||
|
else
|
||||||
|
return new FileStreamSAF(contentResolver, docFile.getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whatever if is possible access using the {@code java.io} API
|
||||||
|
*
|
||||||
|
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
|
||||||
|
*/
|
||||||
|
public boolean isDirect() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
return docFile == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvalid() {
|
||||||
|
return source == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getUri() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void truncate() throws IOException {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
try (SharpStream fs = getStream()) {
|
||||||
|
fs.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
if (docFile == null) return ioFile.delete();
|
||||||
|
|
||||||
|
boolean res = docFile.delete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||||
|
contentResolver.releasePersistableUriPermission(docFile.getUri(), flags);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// ¿what happen?
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long length() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
return docFile == null ? ioFile.length() : docFile.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canWrite() {
|
||||||
|
if (source == null) return false;
|
||||||
|
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getIOFile() {
|
||||||
|
return ioFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
if (source == null) return srcName;
|
||||||
|
return docFile == null ? ioFile.getName() : docFile.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
if (source == null) return srcType;
|
||||||
|
return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsAsFile() {
|
||||||
|
if (source == null) return false;
|
||||||
|
|
||||||
|
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
|
||||||
|
boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
|
||||||
|
|
||||||
|
return exists && asFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean create() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
if (docFile == null) {
|
||||||
|
try {
|
||||||
|
return ioFile.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docTree == null || docFile.getName() == null) return false;
|
||||||
|
|
||||||
|
DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType());
|
||||||
|
if (res == null) return false;
|
||||||
|
|
||||||
|
docFile = res;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidate() {
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
srcName = getName();
|
||||||
|
srcType = getType();
|
||||||
|
|
||||||
|
source = null;
|
||||||
|
|
||||||
|
sourceTree = null;
|
||||||
|
docTree = null;
|
||||||
|
docFile = null;
|
||||||
|
ioFile = null;
|
||||||
|
contentResolver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalid() {
|
||||||
|
if (source == null)
|
||||||
|
throw new IllegalStateException("In invalid state");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(StoredFileHelper storage) {
|
||||||
|
if (this.isInvalid() != storage.isInvalid()) return false;
|
||||||
|
if (this.isDirect() != storage.isDirect()) return false;
|
||||||
|
|
||||||
|
if (this.isDirect())
|
||||||
|
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
|
||||||
|
|
||||||
|
return DocumentsContract.getDocumentId(
|
||||||
|
this.docFile.getUri()
|
||||||
|
).equalsIgnoreCase(DocumentsContract.getDocumentId(
|
||||||
|
storage.docFile.getUri()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (source == null)
|
||||||
|
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
|
||||||
|
else
|
||||||
|
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,12 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
|
|
||||||
public class M4aNoDash extends Postprocessing {
|
public class M4aNoDash extends Postprocessing {
|
||||||
|
|
||||||
M4aNoDash(DownloadMission mission) {
|
M4aNoDash() {
|
||||||
super(mission, 0, true);
|
super(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
class Mp4FromDashMuxer extends Postprocessing {
|
class Mp4FromDashMuxer extends Postprocessing {
|
||||||
|
|
||||||
Mp4FromDashMuxer(DownloadMission mission) {
|
Mp4FromDashMuxer() {
|
||||||
super(mission, 2 * 1024 * 1024/* 2 MiB */, true);
|
super(2 * 1024 * 1024/* 2 MiB */, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package us.shandian.giga.postprocessing;
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
@ -9,9 +10,9 @@ import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
|
import us.shandian.giga.io.ChunkFileInputStream;
|
||||||
import us.shandian.giga.postprocessing.io.CircularFileWriter;
|
import us.shandian.giga.io.CircularFileWriter;
|
||||||
import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker;
|
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
|
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||||
|
@ -20,30 +21,41 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||||
|
|
||||||
public abstract class Postprocessing {
|
public abstract class Postprocessing {
|
||||||
|
|
||||||
static final byte OK_RESULT = ERROR_NOTHING;
|
static transient final byte OK_RESULT = ERROR_NOTHING;
|
||||||
|
|
||||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||||
public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||||
public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||||
|
|
||||||
|
public static Postprocessing getAlgorithm(String algorithmName, String[] args) {
|
||||||
|
Postprocessing instance;
|
||||||
|
|
||||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
|
||||||
if (null == algorithmName) {
|
if (null == algorithmName) {
|
||||||
throw new NullPointerException("algorithmName");
|
throw new NullPointerException("algorithmName");
|
||||||
} else switch (algorithmName) {
|
} else switch (algorithmName) {
|
||||||
case ALGORITHM_TTML_CONVERTER:
|
case ALGORITHM_TTML_CONVERTER:
|
||||||
return new TtmlConverter(mission);
|
instance = new TtmlConverter();
|
||||||
|
break;
|
||||||
case ALGORITHM_WEBM_MUXER:
|
case ALGORITHM_WEBM_MUXER:
|
||||||
return new WebMMuxer(mission);
|
instance = new WebMMuxer();
|
||||||
|
break;
|
||||||
case ALGORITHM_MP4_FROM_DASH_MUXER:
|
case ALGORITHM_MP4_FROM_DASH_MUXER:
|
||||||
return new Mp4FromDashMuxer(mission);
|
instance = new Mp4FromDashMuxer();
|
||||||
|
break;
|
||||||
case ALGORITHM_M4A_NO_DASH:
|
case ALGORITHM_M4A_NO_DASH:
|
||||||
return new M4aNoDash(mission);
|
instance = new M4aNoDash();
|
||||||
|
break;
|
||||||
/*case "example-algorithm":
|
/*case "example-algorithm":
|
||||||
return new ExampleAlgorithm(mission);*/
|
instance = new ExampleAlgorithm(mission);*/
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
|
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance.args = args;
|
||||||
|
instance.name = algorithmName;
|
||||||
|
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,32 +73,38 @@ public abstract class Postprocessing {
|
||||||
/**
|
/**
|
||||||
* the download to post-process
|
* the download to post-process
|
||||||
*/
|
*/
|
||||||
protected DownloadMission mission;
|
protected transient DownloadMission mission;
|
||||||
|
|
||||||
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
public transient File cacheDir;
|
||||||
this.mission = mission;
|
|
||||||
|
private String[] args;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
Postprocessing(int recommendedReserve, boolean worksOnSameFile) {
|
||||||
this.recommendedReserve = recommendedReserve;
|
this.recommendedReserve = recommendedReserve;
|
||||||
this.worksOnSameFile = worksOnSameFile;
|
this.worksOnSameFile = worksOnSameFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() throws IOException {
|
public void run(DownloadMission target) throws IOException {
|
||||||
File file = mission.getDownloadedFile();
|
this.mission = target;
|
||||||
|
|
||||||
File temp = null;
|
File temp = null;
|
||||||
CircularFileWriter out = null;
|
CircularFileWriter out = null;
|
||||||
int result;
|
int result;
|
||||||
long finalLength = -1;
|
long finalLength = -1;
|
||||||
|
|
||||||
mission.done = 0;
|
mission.done = 0;
|
||||||
mission.length = file.length();
|
mission.length = mission.storage.length();
|
||||||
|
|
||||||
if (worksOnSameFile) {
|
if (worksOnSameFile) {
|
||||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||||
try {
|
try {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (; i < sources.length - 1; i++) {
|
for (; i < sources.length - 1; i++) {
|
||||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||||
}
|
}
|
||||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||||
|
|
||||||
if (test(sources)) {
|
if (test(sources)) {
|
||||||
for (SharpStream source : sources) source.rewind();
|
for (SharpStream source : sources) source.rewind();
|
||||||
|
@ -97,7 +115,7 @@ public abstract class Postprocessing {
|
||||||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||||
* or the CircularFileWriter can lead to unexpected results
|
* or the CircularFileWriter can lead to unexpected results
|
||||||
*/
|
*/
|
||||||
if (source.isDisposed() || source.available() < 1) {
|
if (source.isClosed() || source.available() < 1) {
|
||||||
continue;// the selected source is not used anymore
|
continue;// the selected source is not used anymore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,18 +125,19 @@ public abstract class Postprocessing {
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
temp = new File(mission.location, mission.name + ".tmp");
|
// TODO: use Context.getCache() for this operation
|
||||||
|
temp = new File(cacheDir, mission.storage.getName() + ".tmp");
|
||||||
|
|
||||||
out = new CircularFileWriter(file, temp, checker);
|
out = new CircularFileWriter(mission.storage.getStream(), temp, checker);
|
||||||
out.onProgress = this::progressReport;
|
out.onProgress = this::progressReport;
|
||||||
|
|
||||||
out.onWriteError = (err) -> {
|
out.onWriteError = (err) -> {
|
||||||
mission.postprocessingState = 3;
|
mission.psState = 3;
|
||||||
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
|
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
while (mission.postprocessingState == 3)
|
while (mission.psState == 3)
|
||||||
wait();
|
wait();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
@ -138,12 +157,12 @@ public abstract class Postprocessing {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
for (SharpStream source : sources) {
|
for (SharpStream source : sources) {
|
||||||
if (source != null && !source.isDisposed()) {
|
if (source != null && !source.isClosed()) {
|
||||||
source.dispose();
|
source.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out != null) {
|
if (out != null) {
|
||||||
out.dispose();
|
out.close();
|
||||||
}
|
}
|
||||||
if (temp != null) {
|
if (temp != null) {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
@ -164,10 +183,9 @@ public abstract class Postprocessing {
|
||||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != OK_RESULT && worksOnSameFile) {
|
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
file.delete();
|
this.mission = null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -192,11 +210,11 @@ public abstract class Postprocessing {
|
||||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||||
|
|
||||||
String getArgumentAt(int index, String defaultValue) {
|
String getArgumentAt(int index, String defaultValue) {
|
||||||
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
|
if (args == null || index >= args.length) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mission.postprocessingArgs[index];
|
return args[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void progressReport(long done) {
|
private void progressReport(long done) {
|
||||||
|
@ -209,4 +227,22 @@ public abstract class Postprocessing {
|
||||||
|
|
||||||
mission.mHandler.sendMessage(m);
|
mission.mHandler.sendMessage(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
|
||||||
|
str.append("name=").append(name).append('[');
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
for (String arg : args) {
|
||||||
|
str.append(", ");
|
||||||
|
str.append(arg);
|
||||||
|
}
|
||||||
|
str.delete(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.append(']').toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
import org.schabi.newpipe.streams.SubtitleConverter;
|
import org.schabi.newpipe.streams.SubtitleConverter;
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -12,18 +12,15 @@ import java.text.ParseException;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import javax.xml.xpath.XPathExpressionException;
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
import us.shandian.giga.postprocessing.io.SharpInputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
class TtmlConverter extends Postprocessing {
|
class TtmlConverter extends Postprocessing {
|
||||||
private static final String TAG = "TtmlConverter";
|
private static final String TAG = "TtmlConverter";
|
||||||
|
|
||||||
TtmlConverter(DownloadMission mission) {
|
TtmlConverter() {
|
||||||
// due how XmlPullParser works, the xml is fully loaded on the ram
|
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||||
super(mission, 0, true);
|
super(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
class WebMMuxer extends Postprocessing {
|
class WebMMuxer extends Postprocessing {
|
||||||
|
|
||||||
WebMMuxer(DownloadMission mission) {
|
WebMMuxer() {
|
||||||
super(mission, 2048 * 1024/* 2 MiB */, true);
|
super(2048 * 1024/* 2 MiB */, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -13,16 +13,15 @@ import org.schabi.newpipe.R;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
import us.shandian.giga.get.FinishedMission;
|
import us.shandian.giga.get.FinishedMission;
|
||||||
import us.shandian.giga.get.Mission;
|
import us.shandian.giga.get.Mission;
|
||||||
import us.shandian.giga.get.sqlite.DownloadDataSource;
|
import us.shandian.giga.get.sqlite.FinishedMissionStore;
|
||||||
import us.shandian.giga.service.DownloadManagerService.DMChecker;
|
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||||
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
import us.shandian.giga.util.Utility;
|
import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
|
@ -36,7 +35,10 @@ public class DownloadManager {
|
||||||
public final static int SPECIAL_PENDING = 1;
|
public final static int SPECIAL_PENDING = 1;
|
||||||
public final static int SPECIAL_FINISHED = 2;
|
public final static int SPECIAL_FINISHED = 2;
|
||||||
|
|
||||||
private final DownloadDataSource mDownloadDataSource;
|
static final String TAG_AUDIO = "audio";
|
||||||
|
static final String TAG_VIDEO = "video";
|
||||||
|
|
||||||
|
private final FinishedMissionStore mFinishedMissionStore;
|
||||||
|
|
||||||
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
|
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
|
||||||
private final ArrayList<FinishedMission> mMissionsFinished;
|
private final ArrayList<FinishedMission> mMissionsFinished;
|
||||||
|
@ -51,6 +53,9 @@ public class DownloadManager {
|
||||||
boolean mPrefQueueLimit;
|
boolean mPrefQueueLimit;
|
||||||
private boolean mSelfMissionsControl;
|
private boolean mSelfMissionsControl;
|
||||||
|
|
||||||
|
StoredDirectoryHelper mMainStorageAudio;
|
||||||
|
StoredDirectoryHelper mMainStorageVideo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance
|
* Create a new instance
|
||||||
*
|
*
|
||||||
|
@ -62,7 +67,7 @@ public class DownloadManager {
|
||||||
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
|
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
mDownloadDataSource = new DownloadDataSource(context);
|
mFinishedMissionStore = new FinishedMissionStore(context);
|
||||||
mHandler = handler;
|
mHandler = handler;
|
||||||
mMissionsFinished = loadFinishedMissions();
|
mMissionsFinished = loadFinishedMissions();
|
||||||
mPendingMissionsDir = getPendingDir(context);
|
mPendingMissionsDir = getPendingDir(context);
|
||||||
|
@ -71,7 +76,7 @@ public class DownloadManager {
|
||||||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPendingMissions();
|
loadPendingMissions(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File getPendingDir(@NonNull Context context) {
|
private static File getPendingDir(@NonNull Context context) {
|
||||||
|
@ -92,29 +97,24 @@ public class DownloadManager {
|
||||||
* Loads finished missions from the data source
|
* Loads finished missions from the data source
|
||||||
*/
|
*/
|
||||||
private ArrayList<FinishedMission> loadFinishedMissions() {
|
private ArrayList<FinishedMission> loadFinishedMissions() {
|
||||||
ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions();
|
ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions();
|
||||||
|
|
||||||
// missions always is stored by creation order, simply reverse the list
|
// check if the files exists, otherwise, forget the download
|
||||||
ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size());
|
|
||||||
for (int i = finishedMissions.size() - 1; i >= 0; i--) {
|
for (int i = finishedMissions.size() - 1; i >= 0; i--) {
|
||||||
FinishedMission mission = finishedMissions.get(i);
|
FinishedMission mission = finishedMissions.get(i);
|
||||||
File file = mission.getDownloadedFile();
|
|
||||||
|
|
||||||
if (!file.isFile()) {
|
if (!mission.storage.existsAsFile()) {
|
||||||
if (DEBUG) {
|
if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName());
|
||||||
Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath());
|
|
||||||
}
|
mFinishedMissionStore.deleteMission(mission);
|
||||||
mDownloadDataSource.deleteMission(mission);
|
finishedMissions.remove(i);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.add(mission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return finishedMissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPendingMissions() {
|
private void loadPendingMissions(Context ctx) {
|
||||||
File[] subs = mPendingMissionsDir.listFiles();
|
File[] subs = mPendingMissionsDir.listFiles();
|
||||||
|
|
||||||
if (subs == null) {
|
if (subs == null) {
|
||||||
|
@ -142,40 +142,63 @@ public class DownloadManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
File dl = mis.getDownloadedFile();
|
boolean exists;
|
||||||
boolean exists = dl.exists();
|
try {
|
||||||
|
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||||
|
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString());
|
||||||
|
mis.storage.invalidate();
|
||||||
|
exists = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (mis.isPsRunning()) {
|
if (mis.isPsRunning()) {
|
||||||
if (mis.postprocessingThis) {
|
if (mis.psAlgorithm.worksOnSameFile) {
|
||||||
// Incomplete post-processing results in a corrupted download file
|
// Incomplete post-processing results in a corrupted download file
|
||||||
// because the selected algorithm works on the same file to save space.
|
// because the selected algorithm works on the same file to save space.
|
||||||
if (exists && dl.isFile() && !dl.delete())
|
if (exists && !mis.storage.delete())
|
||||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||||
|
|
||||||
exists = true;
|
exists = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
mis.postprocessingState = 0;
|
mis.psState = 0;
|
||||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||||
mis.errObject = null;
|
mis.errObject = null;
|
||||||
} else if (exists && !dl.isFile()) {
|
} else if (!exists) {
|
||||||
// probably a folder, this should never happens
|
|
||||||
if (!sub.delete()) {
|
StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag());
|
||||||
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
|
|
||||||
|
if (!mis.storage.isInvalid() && !mis.storage.create()) {
|
||||||
|
// using javaIO cannot recreate the file
|
||||||
|
// using SAF in older devices (no tree available)
|
||||||
|
//
|
||||||
|
// force the user to pick again the save path
|
||||||
|
mis.storage.invalidate();
|
||||||
|
} else if (mainStorage != null) {
|
||||||
|
// if the user has changed the save path before this download, the original save path will be lost
|
||||||
|
StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType());
|
||||||
|
if (newStorage == null)
|
||||||
|
mis.storage.invalidate();
|
||||||
|
else
|
||||||
|
mis.storage = newStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mis.isInitialized()) {
|
||||||
|
// the progress is lost, reset mission state
|
||||||
|
DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm);
|
||||||
|
m.timestamp = mis.timestamp;
|
||||||
|
m.threadCount = mis.threadCount;
|
||||||
|
m.source = mis.source;
|
||||||
|
m.nearLength = mis.nearLength;
|
||||||
|
m.enqueued = mis.enqueued;
|
||||||
|
m.errCode = DownloadMission.ERROR_PROGRESS_LOST;
|
||||||
|
mis = m;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && mis.isInitialized()) {
|
if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir();
|
||||||
// downloaded file deleted, reset mission state
|
|
||||||
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
|
|
||||||
m.timestamp = mis.timestamp;
|
|
||||||
m.threadCount = mis.threadCount;
|
|
||||||
m.source = mis.source;
|
|
||||||
m.nearLength = mis.nearLength;
|
|
||||||
m.setEnqueued(mis.enqueued);
|
|
||||||
mis = m;
|
|
||||||
}
|
|
||||||
|
|
||||||
mis.running = false;
|
mis.running = false;
|
||||||
mis.recovered = exists;
|
mis.recovered = exists;
|
||||||
|
@ -196,51 +219,15 @@ public class DownloadManager {
|
||||||
/**
|
/**
|
||||||
* Start a new download mission
|
* Start a new download mission
|
||||||
*
|
*
|
||||||
* @param urls the list of urls to download
|
* @param mission the new download mission to add and run (if possible)
|
||||||
* @param location the location
|
|
||||||
* @param name the name of the file to create
|
|
||||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
|
||||||
* @param threads the number of threads maximal used to download chunks of the file.
|
|
||||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
|
||||||
* @param source source url of the resource
|
|
||||||
* @param psArgs the arguments for the post-processing algorithm.
|
|
||||||
*/
|
*/
|
||||||
void startMission(String[] urls, String location, String name, char kind, int threads,
|
void startMission(DownloadMission mission) {
|
||||||
String source, String psName, String[] psArgs, long nearLength) {
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
// check for existing pending download
|
|
||||||
DownloadMission pendingMission = getPendingMission(location, name);
|
|
||||||
|
|
||||||
if (pendingMission != null) {
|
|
||||||
if (pendingMission.running) {
|
|
||||||
// generate unique filename (?)
|
|
||||||
try {
|
|
||||||
name = generateUniqueName(location, name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Unable to generate unique name", e);
|
|
||||||
name = System.currentTimeMillis() + name;
|
|
||||||
Log.i(TAG, "Using " + name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// dispose the mission
|
|
||||||
mMissionsPending.remove(pendingMission);
|
|
||||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
|
||||||
pendingMission.delete();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check for existing finished download and dispose (if exists)
|
|
||||||
int index = getFinishedMissionIndex(location, name);
|
|
||||||
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs);
|
|
||||||
mission.timestamp = System.currentTimeMillis();
|
mission.timestamp = System.currentTimeMillis();
|
||||||
mission.threadCount = threads;
|
|
||||||
mission.source = source;
|
|
||||||
mission.mHandler = mHandler;
|
mission.mHandler = mHandler;
|
||||||
mission.maxRetry = mPrefMaxRetry;
|
mission.maxRetry = mPrefMaxRetry;
|
||||||
mission.nearLength = nearLength;
|
|
||||||
|
|
||||||
|
// create metadata file
|
||||||
while (true) {
|
while (true) {
|
||||||
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
|
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
|
||||||
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
|
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
|
||||||
|
@ -261,6 +248,14 @@ public class DownloadManager {
|
||||||
// Before continue, save the metadata in case the internet connection is not available
|
// Before continue, save the metadata in case the internet connection is not available
|
||||||
Utility.writeToFile(mission.metadata, mission);
|
Utility.writeToFile(mission.metadata, mission);
|
||||||
|
|
||||||
|
if (mission.storage == null) {
|
||||||
|
// noting to do here
|
||||||
|
mission.errCode = DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
if (mission.errObject != null)
|
||||||
|
mission.errObject = new IOException("DownloadMission.storage == NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||||
|
|
||||||
if (canDownloadInCurrentNetwork() && start) {
|
if (canDownloadInCurrentNetwork() && start) {
|
||||||
|
@ -292,7 +287,7 @@ public class DownloadManager {
|
||||||
mMissionsPending.remove(mission);
|
mMissionsPending.remove(mission);
|
||||||
} else if (mission instanceof FinishedMission) {
|
} else if (mission instanceof FinishedMission) {
|
||||||
mMissionsFinished.remove(mission);
|
mMissionsFinished.remove(mission);
|
||||||
mDownloadDataSource.deleteMission(mission);
|
mFinishedMissionStore.deleteMission(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||||
|
@ -300,18 +295,35 @@ public class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void forgetMission(StoredFileHelper storage) {
|
||||||
|
synchronized (this) {
|
||||||
|
Mission mission = getAnyMission(storage);
|
||||||
|
if (mission == null) return;
|
||||||
|
|
||||||
|
if (mission instanceof DownloadMission) {
|
||||||
|
mMissionsPending.remove(mission);
|
||||||
|
} else if (mission instanceof FinishedMission) {
|
||||||
|
mMissionsFinished.remove(mission);
|
||||||
|
mFinishedMissionStore.deleteMission(mission);
|
||||||
|
}
|
||||||
|
|
||||||
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||||
|
mission.storage = null;
|
||||||
|
mission.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a pending mission by its location and name
|
* Get a pending mission by its path
|
||||||
*
|
*
|
||||||
* @param location the location
|
* @param storage where the file possible is stored
|
||||||
* @param name the name
|
|
||||||
* @return the mission or null if no such mission exists
|
* @return the mission or null if no such mission exists
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private DownloadMission getPendingMission(String location, String name) {
|
private DownloadMission getPendingMission(StoredFileHelper storage) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
if (mission.storage.equals(storage)) {
|
||||||
return mission;
|
return mission;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,16 +331,14 @@ public class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a finished mission by its location and name
|
* Get a finished mission by its path
|
||||||
*
|
*
|
||||||
* @param location the location
|
* @param storage where the file possible is stored
|
||||||
* @param name the name
|
|
||||||
* @return the mission index or -1 if no such mission exists
|
* @return the mission index or -1 if no such mission exists
|
||||||
*/
|
*/
|
||||||
private int getFinishedMissionIndex(String location, String name) {
|
private int getFinishedMissionIndex(StoredFileHelper storage) {
|
||||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||||
FinishedMission mission = mMissionsFinished.get(i);
|
if (mMissionsFinished.get(i).storage.equals(storage)) {
|
||||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,12 +346,12 @@ public class DownloadManager {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mission getAnyMission(String location, String name) {
|
private Mission getAnyMission(StoredFileHelper storage) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Mission mission = getPendingMission(location, name);
|
Mission mission = getPendingMission(storage);
|
||||||
if (mission != null) return mission;
|
if (mission != null) return mission;
|
||||||
|
|
||||||
int idx = getFinishedMissionIndex(location, name);
|
int idx = getFinishedMissionIndex(storage);
|
||||||
if (idx >= 0) return mMissionsFinished.get(idx);
|
if (idx >= 0) return mMissionsFinished.get(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +392,7 @@ public class DownloadManager {
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running || mission.isPsFailed() || mission.isFinished()) continue;
|
if (mission.running || !mission.canDownload()) continue;
|
||||||
|
|
||||||
flag = true;
|
flag = true;
|
||||||
mission.start();
|
mission.start();
|
||||||
|
@ -392,58 +402,6 @@ public class DownloadManager {
|
||||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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((dir, name1) -> name1.startsWith(nameParts[0]));
|
|
||||||
Arrays.sort(existingName);
|
|
||||||
String newName;
|
|
||||||
int downloadIndex = 0;
|
|
||||||
do {
|
|
||||||
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
|
|
||||||
++downloadIndex;
|
|
||||||
if (downloadIndex == 1000) { // Probably an error on our side
|
|
||||||
throw new RuntimeException("Too many existing files");
|
|
||||||
}
|
|
||||||
} while (Arrays.binarySearch(existingName, newName) >= 0);
|
|
||||||
return newName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a pending download as finished
|
* Set a pending download as finished
|
||||||
*
|
*
|
||||||
|
@ -453,7 +411,7 @@ public class DownloadManager {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
mMissionsPending.remove(mission);
|
mMissionsPending.remove(mission);
|
||||||
mMissionsFinished.add(0, new FinishedMission(mission));
|
mMissionsFinished.add(0, new FinishedMission(mission));
|
||||||
mDownloadDataSource.addMission(mission);
|
mFinishedMissionStore.addFinishedMission(mission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,7 +432,8 @@ public class DownloadManager {
|
||||||
|
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running || !mission.enqueued || mission.isFinished()) continue;
|
if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage())
|
||||||
|
continue;
|
||||||
|
|
||||||
resumeMission(mission);
|
resumeMission(mission);
|
||||||
if (mPrefQueueLimit) return true;
|
if (mPrefQueueLimit) return true;
|
||||||
|
@ -496,7 +455,7 @@ public class DownloadManager {
|
||||||
public void forgetFinishedDownloads() {
|
public void forgetFinishedDownloads() {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (FinishedMission mission : mMissionsFinished) {
|
for (FinishedMission mission : mMissionsFinished) {
|
||||||
mDownloadDataSource.deleteMission(mission);
|
mFinishedMissionStore.deleteMission(mission);
|
||||||
}
|
}
|
||||||
mMissionsFinished.clear();
|
mMissionsFinished.clear();
|
||||||
}
|
}
|
||||||
|
@ -523,7 +482,7 @@ public class DownloadManager {
|
||||||
int paused = 0;
|
int paused = 0;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.isFinished() || mission.isPsRunning()) continue;
|
if (!mission.canDownload() || mission.isPsRunning()) continue;
|
||||||
|
|
||||||
if (mission.running && isMetered) {
|
if (mission.running && isMetered) {
|
||||||
paused++;
|
paused++;
|
||||||
|
@ -565,24 +524,32 @@ public class DownloadManager {
|
||||||
), Toast.LENGTH_LONG).show();
|
), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkForRunningMission(String location, String name, DMChecker check) {
|
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||||
MissionCheck result = MissionCheck.None;
|
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
DownloadMission pending = getPendingMission(location, name);
|
DownloadMission pending = getPendingMission(storage);
|
||||||
|
|
||||||
if (pending == null) {
|
if (pending == null) {
|
||||||
if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished;
|
if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished;
|
||||||
} else {
|
} else {
|
||||||
if (pending.isFinished()) {
|
if (pending.isFinished()) {
|
||||||
result = MissionCheck.Finished;// this never should happen (race-condition)
|
return MissionState.Finished;// this never should happen (race-condition)
|
||||||
} else {
|
} else {
|
||||||
result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending;
|
return pending.running ? MissionState.PendingRunning : MissionState.Pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
check.callback(result);
|
return MissionState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private StoredDirectoryHelper getMainStorage(@NonNull String tag) {
|
||||||
|
if (tag.equals(TAG_AUDIO)) return mMainStorageAudio;
|
||||||
|
if (tag.equals(TAG_VIDEO)) return mMainStorageVideo;
|
||||||
|
|
||||||
|
Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag));
|
||||||
|
|
||||||
|
return null;// this never should happen
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MissionIterator extends DiffUtil.Callback {
|
public class MissionIterator extends DiffUtil.Callback {
|
||||||
|
@ -689,7 +656,7 @@ public class DownloadManager {
|
||||||
|
|
||||||
synchronized (DownloadManager.this) {
|
synchronized (DownloadManager.this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished())
|
if (hidden.contains(mission) || mission.canDownload())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (mission.running)
|
if (mission.running)
|
||||||
|
@ -720,7 +687,14 @@ public class DownloadManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||||
return areItemsTheSame(oldItemPosition, newItemPosition);
|
Object x = snapshot.get(oldItemPosition);
|
||||||
|
Object y = current.get(newItemPosition);
|
||||||
|
|
||||||
|
if (x instanceof Mission && y instanceof Mission) {
|
||||||
|
return ((Mission) x).storage.equals(((Mission) y).storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,9 @@ import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
@ -21,12 +19,14 @@ import android.net.NetworkRequest;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.support.v4.app.NotificationCompat.Builder;
|
import android.support.v4.app.NotificationCompat.Builder;
|
||||||
import android.support.v4.content.PermissionChecker;
|
import android.support.v4.content.PermissionChecker;
|
||||||
|
@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity;
|
||||||
import org.schabi.newpipe.player.helper.LockManager;
|
import org.schabi.newpipe.player.helper.LockManager;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
|
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||||
|
|
||||||
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
|
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
|
||||||
|
@ -61,19 +65,19 @@ public class DownloadManagerService extends Service {
|
||||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||||
|
|
||||||
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
|
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
|
||||||
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
|
private static final String EXTRA_PATH = "DownloadManagerService.extra.path";
|
||||||
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
|
|
||||||
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
|
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
|
||||||
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
|
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
|
||||||
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
|
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
|
||||||
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
|
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
|
||||||
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
|
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
|
||||||
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
|
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
|
||||||
|
private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag";
|
||||||
|
|
||||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||||
|
|
||||||
private DMBinder mBinder;
|
private DownloadManagerBinder mBinder;
|
||||||
private DownloadManager mManager;
|
private DownloadManager mManager;
|
||||||
private Notification mNotification;
|
private Notification mNotification;
|
||||||
private Handler mHandler;
|
private Handler mHandler;
|
||||||
|
@ -110,10 +114,10 @@ public class DownloadManagerService extends Service {
|
||||||
/**
|
/**
|
||||||
* notify media scanner on downloaded media file ...
|
* notify media scanner on downloaded media file ...
|
||||||
*
|
*
|
||||||
* @param file the downloaded file
|
* @param file the downloaded file uri
|
||||||
*/
|
*/
|
||||||
private void notifyMediaScanner(File file) {
|
private void notifyMediaScanner(Uri file) {
|
||||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
|
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -124,7 +128,7 @@ public class DownloadManagerService extends Service {
|
||||||
Log.d(TAG, "onCreate");
|
Log.d(TAG, "onCreate");
|
||||||
}
|
}
|
||||||
|
|
||||||
mBinder = new DMBinder();
|
mBinder = new DownloadManagerBinder();
|
||||||
mHandler = new Handler(Looper.myLooper()) {
|
mHandler = new Handler(Looper.myLooper()) {
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message msg) {
|
||||||
|
@ -186,10 +190,12 @@ public class DownloadManagerService extends Service {
|
||||||
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
|
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
|
||||||
|
|
||||||
mLock = new LockManager(this);
|
mLock = new LockManager(this);
|
||||||
|
|
||||||
|
setupStorageAPI(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, intent == null ? "Restarting" : "Starting");
|
Log.d(TAG, intent == null ? "Restarting" : "Starting");
|
||||||
}
|
}
|
||||||
|
@ -200,20 +206,7 @@ public class DownloadManagerService extends Service {
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
if (action.equals(Intent.ACTION_RUN)) {
|
if (action.equals(Intent.ACTION_RUN)) {
|
||||||
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
mHandler.post(() -> startMission(intent));
|
||||||
String name = intent.getStringExtra(EXTRA_NAME);
|
|
||||||
String location = intent.getStringExtra(EXTRA_LOCATION);
|
|
||||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
|
||||||
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
|
||||||
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
|
||||||
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
|
||||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
|
||||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
|
||||||
|
|
||||||
handleConnectivityState(true);// first check the actual network status
|
|
||||||
|
|
||||||
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength));
|
|
||||||
|
|
||||||
} else if (downloadDoneNotification != null) {
|
} else if (downloadDoneNotification != null) {
|
||||||
if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) {
|
if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) {
|
||||||
downloadDoneCount = 0;
|
downloadDoneCount = 0;
|
||||||
|
@ -264,12 +257,12 @@ public class DownloadManagerService extends Service {
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
int permissionCheck;
|
int permissionCheck;
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||||
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||||
|
@ -284,8 +277,8 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MESSAGE_FINISHED:
|
case MESSAGE_FINISHED:
|
||||||
notifyMediaScanner(mission.getDownloadedFile());
|
notifyMediaScanner(mission.storage.getUri());
|
||||||
notifyFinishedDownload(mission.name);
|
notifyFinishedDownload(mission.storage.getName());
|
||||||
mManager.setFinished(mission);
|
mManager.setFinished(mission);
|
||||||
handleConnectivityState(false);
|
handleConnectivityState(false);
|
||||||
updateForegroundState(mManager.runMissions());
|
updateForegroundState(mManager.runMissions());
|
||||||
|
@ -344,7 +337,7 @@ public class DownloadManagerService extends Service {
|
||||||
if (key.equals(getString(R.string.downloads_maximum_retry))) {
|
if (key.equals(getString(R.string.downloads_maximum_retry))) {
|
||||||
try {
|
try {
|
||||||
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
|
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
|
||||||
mManager.mPrefMaxRetry = Integer.parseInt(value);
|
mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
mManager.mPrefMaxRetry = 0;
|
mManager.mPrefMaxRetry = 0;
|
||||||
}
|
}
|
||||||
|
@ -353,6 +346,12 @@ public class DownloadManagerService extends Service {
|
||||||
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
|
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
|
||||||
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
|
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
|
||||||
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
|
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
|
||||||
|
} else if (key.equals(getString(R.string.downloads_storage_api))) {
|
||||||
|
setupStorageAPI(false);
|
||||||
|
} else if (key.equals(getString(R.string.download_path_video_key))) {
|
||||||
|
loadMainStorage(key, DownloadManager.TAG_VIDEO, false);
|
||||||
|
} else if (key.equals(getString(R.string.download_path_audio_key))) {
|
||||||
|
loadMainStorage(key, DownloadManager.TAG_AUDIO, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,43 +369,61 @@ public class DownloadManagerService extends Service {
|
||||||
mForeground = state;
|
mForeground = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startMission(Context context, String urls[], String location, String name, char kind,
|
/**
|
||||||
|
* Start a new download mission
|
||||||
|
*
|
||||||
|
* @param context the activity context
|
||||||
|
* @param urls the list of urls to download
|
||||||
|
* @param storage where the file is saved
|
||||||
|
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||||
|
* @param threads the number of threads maximal used to download chunks of the file.
|
||||||
|
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||||
|
* @param source source url of the resource
|
||||||
|
* @param psArgs the arguments for the post-processing algorithm.
|
||||||
|
* @param nearLength the approximated final length of the file
|
||||||
|
*/
|
||||||
|
public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind,
|
||||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||||
intent.setAction(Intent.ACTION_RUN);
|
intent.setAction(Intent.ACTION_RUN);
|
||||||
intent.putExtra(EXTRA_URLS, urls);
|
intent.putExtra(EXTRA_URLS, urls);
|
||||||
intent.putExtra(EXTRA_NAME, name);
|
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||||
intent.putExtra(EXTRA_LOCATION, location);
|
|
||||||
intent.putExtra(EXTRA_KIND, kind);
|
intent.putExtra(EXTRA_KIND, kind);
|
||||||
intent.putExtra(EXTRA_THREADS, threads);
|
intent.putExtra(EXTRA_THREADS, threads);
|
||||||
intent.putExtra(EXTRA_SOURCE, source);
|
intent.putExtra(EXTRA_SOURCE, source);
|
||||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||||
|
intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag());
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) {
|
public void startMission(Intent intent) {
|
||||||
Intent intent = new Intent();
|
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
||||||
intent.setClass(context, DownloadManagerService.class);
|
Uri path = intent.getParcelableExtra(EXTRA_PATH);
|
||||||
context.startService(intent);
|
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||||
|
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
||||||
|
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
||||||
|
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
||||||
|
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||||
|
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||||
|
String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG);
|
||||||
|
|
||||||
context.bindService(intent, new ServiceConnection() {
|
StoredFileHelper storage;
|
||||||
@Override
|
try {
|
||||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
storage = new StoredFileHelper(this, path, tag);
|
||||||
try {
|
} catch (IOException e) {
|
||||||
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker);
|
throw new RuntimeException(e);// this never should happen
|
||||||
} catch (Exception err) {
|
}
|
||||||
Log.w(TAG, "checkForRunningMission() callback is defective", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.unbindService(this);
|
final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs));
|
||||||
}
|
mission.threadCount = threads;
|
||||||
|
mission.source = source;
|
||||||
|
mission.nearLength = nearLength;
|
||||||
|
|
||||||
@Override
|
handleConnectivityState(true);// first check the actual network status
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
}
|
mManager.startMission(mission);
|
||||||
}, Context.BIND_AUTO_CREATE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyFinishedDownload(String name) {
|
public void notifyFinishedDownload(String name) {
|
||||||
|
@ -471,12 +488,12 @@ public class DownloadManagerService extends Service {
|
||||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
downloadFailedNotification.setContentTitle(getString(R.string.app_name));
|
downloadFailedNotification.setContentTitle(getString(R.string.app_name));
|
||||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.name)));
|
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName())));
|
||||||
} else {
|
} else {
|
||||||
downloadFailedNotification.setContentTitle(getString(R.string.download_failed));
|
downloadFailedNotification.setContentTitle(getString(R.string.download_failed));
|
||||||
downloadFailedNotification.setContentText(mission.name);
|
downloadFailedNotification.setContentText(mission.storage.getName());
|
||||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
.bigText(mission.name));
|
.bigText(mission.storage.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
mNotificationManager.notify(id, downloadFailedNotification.build());
|
mNotificationManager.notify(id, downloadFailedNotification.build());
|
||||||
|
@ -508,16 +525,81 @@ public class DownloadManagerService extends Service {
|
||||||
mLockAcquired = acquire;
|
mLockAcquired = acquire;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupStorageAPI(boolean acquire) {
|
||||||
|
loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire);
|
||||||
|
loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadMainStorage(String prefKey, String tag, boolean acquire) {
|
||||||
|
String path = mPrefs.getString(prefKey, null);
|
||||||
|
|
||||||
|
final String JAVA_IO = getString(R.string.downloads_storage_api_default);
|
||||||
|
boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO));
|
||||||
|
|
||||||
|
final String defaultPath;
|
||||||
|
if (tag.equals(DownloadManager.TAG_VIDEO))
|
||||||
|
defaultPath = Environment.DIRECTORY_MOVIES;
|
||||||
|
else// if (tag.equals(DownloadManager.TAG_AUDIO))
|
||||||
|
defaultPath = Environment.DIRECTORY_MUSIC;
|
||||||
|
|
||||||
|
StoredDirectoryHelper mainStorage;
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (path.charAt(0) == File.separatorChar) {
|
||||||
|
Log.i(TAG, "Migrating old save path: " + path);
|
||||||
|
|
||||||
|
useJavaIO = true;
|
||||||
|
path = Uri.fromFile(new File(path)).toString();
|
||||||
|
|
||||||
|
mPrefs.edit().putString(prefKey, path).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useJavaIO) {
|
||||||
|
mainStorage = new StoredDirectoryHelper(path, tag);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// tree api is not available in older versions
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
mainStorage = null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag);
|
||||||
|
if (acquire) mainStorage.acquirePermissions();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e);
|
||||||
|
mainStorage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.equals(DownloadManager.TAG_VIDEO))
|
||||||
|
mManager.mMainStorageVideo = mainStorage;
|
||||||
|
else// if (tag.equals(DownloadManager.TAG_AUDIO))
|
||||||
|
mManager.mMainStorageAudio = mainStorage;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Wrappers for DownloadManager
|
// Wrappers for DownloadManager
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public class DMBinder extends Binder {
|
public class DownloadManagerBinder extends Binder {
|
||||||
public DownloadManager getDownloadManager() {
|
public DownloadManager getDownloadManager() {
|
||||||
return mManager;
|
return mManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public StoredDirectoryHelper getMainStorageVideo() {
|
||||||
|
return mManager.mMainStorageVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public StoredDirectoryHelper getMainStorageAudio() {
|
||||||
|
return mManager.mMainStorageAudio;
|
||||||
|
}
|
||||||
|
|
||||||
public void addMissionEventListener(Handler handler) {
|
public void addMissionEventListener(Handler handler) {
|
||||||
manageObservers(handler, true);
|
manageObservers(handler, true);
|
||||||
}
|
}
|
||||||
|
@ -548,10 +630,4 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface DMChecker {
|
|
||||||
void callback(MissionCheck result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum MissionCheck {None, Pending, PendingRunning, Finished}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package us.shandian.giga.service;
|
||||||
|
|
||||||
|
public enum MissionState {
|
||||||
|
None, Pending, PendingRunning, Finished
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
@ -49,6 +48,7 @@ import java.util.Collections;
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
import us.shandian.giga.get.FinishedMission;
|
import us.shandian.giga.get.FinishedMission;
|
||||||
import us.shandian.giga.get.Mission;
|
import us.shandian.giga.get.Mission;
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
import us.shandian.giga.service.DownloadManager;
|
import us.shandian.giga.service.DownloadManager;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.ui.common.Deleter;
|
import us.shandian.giga.ui.common.Deleter;
|
||||||
|
@ -69,6 +69,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||||
|
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
||||||
|
@ -97,8 +98,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
private MenuItem mStartButton;
|
private MenuItem mStartButton;
|
||||||
private MenuItem mPauseButton;
|
private MenuItem mPauseButton;
|
||||||
private View mEmptyMessage;
|
private View mEmptyMessage;
|
||||||
|
private RecoverHelper mRecover;
|
||||||
|
|
||||||
public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) {
|
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mDownloadManager = downloadManager;
|
mDownloadManager = downloadManager;
|
||||||
mDeleter = null;
|
mDeleter = null;
|
||||||
|
@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
|
|
||||||
if (h.item.mission instanceof DownloadMission) {
|
if (h.item.mission instanceof DownloadMission) {
|
||||||
mPendingDownloadsItems.remove(h);
|
mPendingDownloadsItems.remove(h);
|
||||||
if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false);
|
if (mPendingDownloadsItems.size() < 1) {
|
||||||
|
setAutoRefresh(false);
|
||||||
|
if (mStartButton != null) mStartButton.setVisible(false);
|
||||||
|
if (mPauseButton != null) mPauseButton.setVisible(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.popupMenu.dismiss();
|
h.popupMenu.dismiss();
|
||||||
|
@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
ViewHolderItem h = (ViewHolderItem) view;
|
ViewHolderItem h = (ViewHolderItem) view;
|
||||||
h.item = item;
|
h.item = item;
|
||||||
|
|
||||||
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name);
|
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName());
|
||||||
|
|
||||||
h.icon.setImageResource(Utility.getIconForFileType(type));
|
h.icon.setImageResource(Utility.getIconForFileType(type));
|
||||||
h.name.setText(item.mission.name);
|
h.name.setText(item.mission.storage.getName());
|
||||||
|
|
||||||
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
|
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
|
||||||
|
|
||||||
|
@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
long length = mission.getLength();
|
long length = mission.getLength();
|
||||||
|
|
||||||
int state;
|
int state;
|
||||||
if (mission.isPsFailed()) {
|
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
|
||||||
state = 0;
|
state = 0;
|
||||||
} else if (!mission.running) {
|
} else if (!mission.running) {
|
||||||
state = mission.enqueued ? 1 : 2;
|
state = mission.enqueued ? 1 : 2;
|
||||||
|
@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
||||||
|
|
||||||
Uri uri = FileProvider.getUriForFile(
|
Uri uri;
|
||||||
mContext,
|
|
||||||
BuildConfig.APPLICATION_ID + ".provider",
|
if (mission.storage.isDirect()) {
|
||||||
mission.getDownloadedFile()
|
uri = FileProvider.getUriForFile(
|
||||||
);
|
mContext,
|
||||||
|
BuildConfig.APPLICATION_ID + ".provider",
|
||||||
|
mission.storage.getIOFile()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
uri = mission.storage.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
|
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.setType(resolveMimeType(mission));
|
intent.setType(resolveMimeType(mission));
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI());
|
intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri());
|
||||||
|
|
||||||
mContext.startActivity(Intent.createChooser(intent, null));
|
mContext.startActivity(Intent.createChooser(intent, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String resolveMimeType(@NonNull Mission mission) {
|
private static String resolveMimeType(@NonNull Mission mission) {
|
||||||
String ext = Utility.getFileExt(mission.getDownloadedFile().getName());
|
String ext = Utility.getFileExt(mission.storage.getName());
|
||||||
if (ext == null) return DEFAULT_MIME_TYPE;
|
if (ext == null) return DEFAULT_MIME_TYPE;
|
||||||
|
|
||||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
|
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
|
||||||
|
@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkInvalidFile(@NonNull Mission mission) {
|
private boolean checkInvalidFile(@NonNull Mission mission) {
|
||||||
if (mission.getDownloadedFile().exists()) return false;
|
if (mission.storage.existsAsFile()) return false;
|
||||||
|
|
||||||
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
||||||
return true;
|
return true;
|
||||||
|
@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
case ERROR_UNKNOWN_EXCEPTION:
|
case ERROR_UNKNOWN_EXCEPTION:
|
||||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||||
return;
|
return;
|
||||||
|
case ERROR_PROGRESS_LOST:
|
||||||
|
msg = R.string.error_progress_lost;
|
||||||
default:
|
default:
|
||||||
if (mission.errCode >= 100 && mission.errCode < 600) {
|
if (mission.errCode >= 100 && mission.errCode < 600) {
|
||||||
msgEx = "HTTP " + mission.errCode;
|
msgEx = "HTTP " + mission.errCode;
|
||||||
|
@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
||||||
.setTitle(mission.name)
|
.setTitle(mission.storage.getName())
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
updateProgress(h);
|
updateProgress(h);
|
||||||
return true;
|
return true;
|
||||||
case R.id.retry:
|
case R.id.retry:
|
||||||
|
if (mission.hasInvalidStorage()) {
|
||||||
|
mRecover.tryRecover(mission);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
mission.psContinue(true);
|
mission.psContinue(true);
|
||||||
return true;
|
return true;
|
||||||
case R.id.cancel:
|
case R.id.cancel:
|
||||||
|
@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
return true;
|
return true;
|
||||||
case R.id.md5:
|
case R.id.md5:
|
||||||
case R.id.sha1:
|
case R.id.sha1:
|
||||||
new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id));
|
new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id));
|
||||||
return true;
|
return true;
|
||||||
case R.id.source:
|
case R.id.source:
|
||||||
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
|
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
|
||||||
|
@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void deleterDispose(Bundle bundle) {
|
public void deleterDispose(boolean commitChanges) {
|
||||||
if (mDeleter != null) mDeleter.dispose(bundle);
|
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleterLoad(Bundle bundle, View view) {
|
public void deleterLoad(View view) {
|
||||||
if (mDeleter == null)
|
if (mDeleter == null)
|
||||||
mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler);
|
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleterResume() {
|
public void deleterResume() {
|
||||||
if (mDeleter != null) mDeleter.resume();
|
if (mDeleter != null) mDeleter.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) {
|
||||||
|
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||||
|
if (mission != h.item.mission) continue;
|
||||||
|
|
||||||
|
mission.changeStorage(newStorage);
|
||||||
|
mission.errCode = DownloadMission.ERROR_NOTHING;
|
||||||
|
mission.errObject = null;
|
||||||
|
|
||||||
|
h.status.setText(UNDEFINED_PROGRESS);
|
||||||
|
h.state = -1;
|
||||||
|
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||||
|
h.progress.setMarquee(true);
|
||||||
|
|
||||||
|
mDownloadManager.resumeMission(mission);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean mUpdaterRunning = false;
|
private boolean mUpdaterRunning = false;
|
||||||
private final Runnable rUpdater = this::updater;
|
private final Runnable rUpdater = this::updater;
|
||||||
|
@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
return Float.isNaN(value) || Float.isInfinite(value);
|
return Float.isNaN(value) || Float.isInfinite(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecover(@NonNull RecoverHelper callback) {
|
||||||
|
mRecover = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ViewHolderItem extends RecyclerView.ViewHolder {
|
class ViewHolderItem extends RecyclerView.ViewHolder {
|
||||||
DownloadManager.MissionItem item;
|
DownloadManager.MissionItem item;
|
||||||
|
@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
||||||
|
|
||||||
if (mission != null) {
|
if (mission != null) {
|
||||||
if (mission.isPsRunning()) {
|
if (mission.hasInvalidStorage()) {
|
||||||
|
retry.setEnabled(true);
|
||||||
|
delete.setEnabled(true);
|
||||||
|
showError.setEnabled(true);
|
||||||
|
} else if (mission.isPsRunning()) {
|
||||||
switch (mission.errCode) {
|
switch (mission.errCode) {
|
||||||
case ERROR_INSUFFICIENT_STORAGE:
|
case ERROR_INSUFFICIENT_STORAGE:
|
||||||
case ERROR_POSTPROCESSING_HOLD:
|
case ERROR_POSTPROCESSING_HOLD:
|
||||||
|
@ -838,7 +883,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static class ChecksumTask extends AsyncTask<String, Void, String> {
|
static class ChecksumTask extends AsyncTask<Object, Void, String> {
|
||||||
ProgressDialog progressDialog;
|
ProgressDialog progressDialog;
|
||||||
WeakReference<Activity> weakReference;
|
WeakReference<Activity> weakReference;
|
||||||
|
|
||||||
|
@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(String... params) {
|
protected String doInBackground(Object... params) {
|
||||||
return Utility.checksum(params[0], params[1]);
|
return Utility.checksum((StoredFileHelper) params[0], (String) params[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -889,4 +934,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface RecoverHelper {
|
||||||
|
void tryRecover(DownloadMission mission);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package us.shandian.giga.ui.common;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -23,8 +21,6 @@ public class Deleter {
|
||||||
private static final int TIMEOUT = 5000;// ms
|
private static final int TIMEOUT = 5000;// ms
|
||||||
private static final int DELAY = 350;// ms
|
private static final int DELAY = 350;// ms
|
||||||
private static final int DELAY_RESUME = 400;// 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 Snackbar snackbar;
|
||||||
private ArrayList<Mission> items;
|
private ArrayList<Mission> items;
|
||||||
|
@ -41,7 +37,7 @@ public class Deleter {
|
||||||
private final Runnable rNext;
|
private final Runnable rNext;
|
||||||
private final Runnable rCommit;
|
private final Runnable rCommit;
|
||||||
|
|
||||||
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
||||||
mView = v;
|
mView = v;
|
||||||
mContext = c;
|
mContext = c;
|
||||||
mAdapter = a;
|
mAdapter = a;
|
||||||
|
@ -55,27 +51,6 @@ public class Deleter {
|
||||||
rCommit = this::commit;
|
rCommit = this::commit;
|
||||||
|
|
||||||
items = new ArrayList<>(2);
|
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) {
|
public void append(Mission item) {
|
||||||
|
@ -104,7 +79,7 @@ public class Deleter {
|
||||||
private void next() {
|
private void next() {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
|
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
|
||||||
|
|
||||||
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||||
snackbar.setAction(R.string.undo, s -> forget());
|
snackbar.setAction(R.string.undo, s -> forget());
|
||||||
|
@ -125,7 +100,7 @@ public class Deleter {
|
||||||
mDownloadManager.deleteMission(mission);
|
mDownloadManager.deleteMission(mission);
|
||||||
|
|
||||||
if (mission instanceof FinishedMission) {
|
if (mission instanceof FinishedMission) {
|
||||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
|
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -151,27 +126,14 @@ public class Deleter {
|
||||||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose(Bundle bundle) {
|
public void dispose(boolean commitChanges) {
|
||||||
if (items.size() < 1) return;
|
if (items.size() < 1) return;
|
||||||
|
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
if (bundle == null) {
|
if (!commitChanges) return;
|
||||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
|
||||||
items = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] names = new String[items.size()];
|
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||||
String[] locations = new String[items.size()];
|
items = null;
|
||||||
|
|
||||||
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,7 +1,6 @@
|
||||||
package us.shandian.giga.ui.fragment;
|
package us.shandian.giga.ui.fragment;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -10,6 +9,7 @@ import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v7.widget.GridLayoutManager;
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
@ -18,18 +18,24 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import us.shandian.giga.get.DownloadMission;
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
import us.shandian.giga.service.DownloadManager;
|
import us.shandian.giga.service.DownloadManager;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.service.DownloadManagerService.DMBinder;
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||||
import us.shandian.giga.ui.adapter.MissionAdapter;
|
import us.shandian.giga.ui.adapter.MissionAdapter;
|
||||||
|
|
||||||
public class MissionsFragment extends Fragment {
|
public class MissionsFragment extends Fragment {
|
||||||
|
|
||||||
private static final int SPAN_SIZE = 2;
|
private static final int SPAN_SIZE = 2;
|
||||||
|
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
|
||||||
|
|
||||||
private SharedPreferences mPrefs;
|
private SharedPreferences mPrefs;
|
||||||
private boolean mLinear;
|
private boolean mLinear;
|
||||||
|
@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment {
|
||||||
private LinearLayoutManager mLinearManager;
|
private LinearLayoutManager mLinearManager;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
|
||||||
private DMBinder mBinder;
|
private DownloadManagerBinder mBinder;
|
||||||
private Bundle mBundle;
|
|
||||||
private boolean mForceUpdate;
|
private boolean mForceUpdate;
|
||||||
|
|
||||||
|
private DownloadMission unsafeMissionTarget = null;
|
||||||
|
|
||||||
private ServiceConnection mConnection = new ServiceConnection() {
|
private ServiceConnection mConnection = new ServiceConnection() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
mBinder = (DownloadManagerBinder) binder;
|
||||||
mBinder.clearDownloadNotifications();
|
mBinder.clearDownloadNotifications();
|
||||||
|
|
||||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||||
mAdapter.deleterLoad(mBundle, getView());
|
mAdapter.deleterLoad(getView());
|
||||||
|
|
||||||
|
mAdapter.setRecover(mission ->
|
||||||
|
StoredFileHelper.requestSafWithFileCreation(
|
||||||
|
MissionsFragment.this,
|
||||||
|
REQUEST_DOWNLOAD_PATH_SAF,
|
||||||
|
mission.storage.getName(),
|
||||||
|
mission.storage.getType()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
setAdapterButtons();
|
setAdapterButtons();
|
||||||
|
|
||||||
mBundle = null;
|
|
||||||
|
|
||||||
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
||||||
mBinder.enableNotifications(false);
|
mBinder.enableNotifications(false);
|
||||||
|
|
||||||
|
@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment {
|
||||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
mLinear = mPrefs.getBoolean("linear", false);
|
mLinear = mPrefs.getBoolean("linear", false);
|
||||||
|
|
||||||
//mContext = getActivity().getApplicationContext();
|
|
||||||
mBundle = savedInstanceState;
|
|
||||||
|
|
||||||
// Bind the service
|
// Bind the service
|
||||||
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment {
|
||||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||||
mBinder.enableNotifications(true);
|
mBinder.enableNotifications(true);
|
||||||
mContext.unbindService(mConnection);
|
mContext.unbindService(mConnection);
|
||||||
mAdapter.deleterDispose(null);
|
mAdapter.deleterDispose(true);
|
||||||
|
|
||||||
mBinder = null;
|
mBinder = null;
|
||||||
mAdapter = null;
|
mAdapter = null;
|
||||||
|
@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment {
|
||||||
return true;
|
return true;
|
||||||
case R.id.start_downloads:
|
case R.id.start_downloads:
|
||||||
item.setVisible(false);
|
item.setVisible(false);
|
||||||
|
mPause.setVisible(true);
|
||||||
mBinder.getDownloadManager().startAllMissions();
|
mBinder.getDownloadManager().startAllMissions();
|
||||||
return true;
|
return true;
|
||||||
case R.id.pause_downloads:
|
case R.id.pause_downloads:
|
||||||
item.setVisible(false);
|
item.setVisible(false);
|
||||||
|
mStart.setVisible(true);
|
||||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||||
mAdapter.ensurePausedMissions();// update items view
|
mAdapter.ensurePausedMissions();// update items view
|
||||||
default:
|
default:
|
||||||
|
@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
if (mAdapter != null) {
|
if (mAdapter != null) {
|
||||||
mAdapter.deleterDispose(outState);
|
mAdapter.deleterDispose(false);
|
||||||
mForceUpdate = true;
|
mForceUpdate = true;
|
||||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||||
}
|
}
|
||||||
|
@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment {
|
||||||
if (mAdapter != null) mAdapter.onPaused();
|
if (mAdapter != null) mAdapter.onPaused();
|
||||||
if (mBinder != null) mBinder.enableNotifications(true);
|
if (mBinder != null) mBinder.enableNotifications(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return;
|
||||||
|
|
||||||
|
if (unsafeMissionTarget == null || data.getData() == null) {
|
||||||
|
return;// unsafeMissionTarget cannot be null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag());
|
||||||
|
mAdapter.recoverMission(unsafeMissionTarget, storage);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ import android.support.v4.content.ContextCompat;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
|
@ -25,7 +25,8 @@ import java.io.Serializable;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Locale;
|
|
||||||
|
import us.shandian.giga.io.StoredFileHelper;
|
||||||
|
|
||||||
public class Utility {
|
public class Utility {
|
||||||
|
|
||||||
|
@ -206,7 +207,7 @@ public class Utility {
|
||||||
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String checksum(String path, String algorithm) {
|
public static String checksum(StoredFileHelper source, String algorithm) {
|
||||||
MessageDigest md;
|
MessageDigest md;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -215,11 +216,11 @@ public class Utility {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileInputStream i;
|
SharpStream i;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
i = new FileInputStream(path);
|
i = source.getStream();
|
||||||
} catch (FileNotFoundException e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,15 +248,15 @@ public class Utility {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
public static boolean mkdir(File path, boolean allDirs) {
|
public static boolean mkdir(File p, boolean allDirs) {
|
||||||
if (path.exists()) return true;
|
if (p.exists()) return true;
|
||||||
|
|
||||||
if (allDirs)
|
if (allDirs)
|
||||||
path.mkdirs();
|
p.mkdirs();
|
||||||
else
|
else
|
||||||
path.mkdir();
|
p.mkdir();
|
||||||
|
|
||||||
return path.exists();
|
return p.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getContentLength(HttpURLConnection connection) {
|
public static long getContentLength(HttpURLConnection connection) {
|
||||||
|
|
|
@ -462,12 +462,12 @@
|
||||||
<string name="download_finished_more">%s أنتهى التحميل</string>
|
<string name="download_finished_more">%s أنتهى التحميل</string>
|
||||||
<string name="generate_unique_name">إنشاء اسم فريد</string>
|
<string name="generate_unique_name">إنشاء اسم فريد</string>
|
||||||
<string name="overwrite">الكتابة فوق</string>
|
<string name="overwrite">الكتابة فوق</string>
|
||||||
<string name="overwrite_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
|
<string name="overwrite_finished_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
|
||||||
<string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string>
|
<string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string>
|
||||||
<string name="show_error">إظهار خطأ</string>
|
<string name="show_error">إظهار خطأ</string>
|
||||||
<string name="label_code">كود</string>
|
<string name="label_code">كود</string>
|
||||||
<string name="error_path_creation">لا يمكن إنشاء الملف</string>
|
<string name="error_file_creation">لا يمكن إنشاء الملف</string>
|
||||||
<string name="error_file_creation">لا يمكن إنشاء المجلد الوجهة</string>
|
<string name="error_path_creation">لا يمكن إنشاء المجلد الوجهة</string>
|
||||||
<string name="error_permission_denied">تم رفضها من قبل النظام</string>
|
<string name="error_permission_denied">تم رفضها من قبل النظام</string>
|
||||||
<string name="error_ssl_exception">فشل اتصال الأمن</string>
|
<string name="error_ssl_exception">فشل اتصال الأمن</string>
|
||||||
<string name="error_unknown_host">تعذر العثور على الخادم</string>
|
<string name="error_unknown_host">تعذر العثور على الخادم</string>
|
||||||
|
|
|
@ -432,8 +432,8 @@
|
||||||
<string name="generate_unique_name">Genera un nom únic</string>
|
<string name="generate_unique_name">Genera un nom únic</string>
|
||||||
<string name="show_error">Mostra l\'error</string>
|
<string name="show_error">Mostra l\'error</string>
|
||||||
<string name="label_code">Codi</string>
|
<string name="label_code">Codi</string>
|
||||||
<string name="error_path_creation">No es pot crear el fitxer</string>
|
<string name="error_file_creation">No es pot crear el fitxer</string>
|
||||||
<string name="error_file_creation">No es pot crear la carpeta de destinació</string>
|
<string name="error_path_creation">No es pot crear la carpeta de destinació</string>
|
||||||
<string name="stop">Atura</string>
|
<string name="stop">Atura</string>
|
||||||
<string name="events">Esdeveniments</string>
|
<string name="events">Esdeveniments</string>
|
||||||
<string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string>
|
<string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string>
|
||||||
|
|
|
@ -437,11 +437,11 @@
|
||||||
<string name="download_finished_more">%s已下载完毕</string>
|
<string name="download_finished_more">%s已下载完毕</string>
|
||||||
<string name="generate_unique_name">生成独特的名字</string>
|
<string name="generate_unique_name">生成独特的名字</string>
|
||||||
<string name="overwrite">覆写</string>
|
<string name="overwrite">覆写</string>
|
||||||
<string name="overwrite_warning">同名的已下载文件已经存在</string>
|
<string name="overwrite_finished_warning">同名的已下载文件已经存在</string>
|
||||||
<string name="download_already_running">同名下载进行中</string>
|
<string name="download_already_running">同名下载进行中</string>
|
||||||
<string name="show_error">显示错误</string>
|
<string name="show_error">显示错误</string>
|
||||||
<string name="label_code">代码</string>
|
<string name="label_code">代码</string>
|
||||||
<string name="error_path_creation">无法创建该文件</string>
|
<string name="error_file_creation">无法创建该文件</string>
|
||||||
<string name="error_permission_denied">系统拒绝此批准</string>
|
<string name="error_permission_denied">系统拒绝此批准</string>
|
||||||
<string name="error_ssl_exception">安全连接失败</string>
|
<string name="error_ssl_exception">安全连接失败</string>
|
||||||
<string name="error_unknown_host">找不到服务器</string>
|
<string name="error_unknown_host">找不到服务器</string>
|
||||||
|
@ -464,7 +464,7 @@
|
||||||
<string name="grid">网格</string>
|
<string name="grid">网格</string>
|
||||||
<string name="switch_view">切换视图</string>
|
<string name="switch_view">切换视图</string>
|
||||||
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
||||||
<string name="error_file_creation">无法创建目标文件夹</string>
|
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string>
|
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string>
|
||||||
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
|
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
|
||||||
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
||||||
|
|
|
@ -372,8 +372,8 @@
|
||||||
<string name="download_already_running">Der er en download i gang med dette navn</string>
|
<string name="download_already_running">Der er en download i gang med dette navn</string>
|
||||||
<string name="show_error">Vis fejl</string>
|
<string name="show_error">Vis fejl</string>
|
||||||
<string name="label_code">Kode</string>
|
<string name="label_code">Kode</string>
|
||||||
<string name="error_path_creation">Filen kan ikke oprettes</string>
|
<string name="error_file_creation">Filen kan ikke oprettes</string>
|
||||||
<string name="error_file_creation">Destinationsmappen kan ikke oprettes</string>
|
<string name="error_path_creation">Destinationsmappen kan ikke oprettes</string>
|
||||||
<string name="error_permission_denied">Adgang nægtet af systemet</string>
|
<string name="error_permission_denied">Adgang nægtet af systemet</string>
|
||||||
<string name="error_ssl_exception">Sikker forbindelse fejlede</string>
|
<string name="error_ssl_exception">Sikker forbindelse fejlede</string>
|
||||||
<string name="error_unknown_host">Kunne ikke finde serveren</string>
|
<string name="error_unknown_host">Kunne ikke finde serveren</string>
|
||||||
|
|
|
@ -448,12 +448,12 @@
|
||||||
<string name="download_finished_more">%s heruntergeladen</string>
|
<string name="download_finished_more">%s heruntergeladen</string>
|
||||||
<string name="generate_unique_name">Eindeutigen Namen erzeugen</string>
|
<string name="generate_unique_name">Eindeutigen Namen erzeugen</string>
|
||||||
<string name="overwrite">Überschreiben</string>
|
<string name="overwrite">Überschreiben</string>
|
||||||
<string name="overwrite_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string>
|
<string name="overwrite_finished_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string>
|
||||||
<string name="download_already_running">Eine Datei dieses Namens wird gerade heruntergeladen</string>
|
<string name="download_already_running">Eine Datei dieses Namens wird gerade heruntergeladen</string>
|
||||||
<string name="show_error">Fehler anzeigen</string>
|
<string name="show_error">Fehler anzeigen</string>
|
||||||
<string name="label_code">Code</string>
|
<string name="label_code">Code</string>
|
||||||
<string name="error_path_creation">Die Datei kann nicht erstellt werden</string>
|
<string name="error_file_creation">Die Datei kann nicht erstellt werden</string>
|
||||||
<string name="error_file_creation">Der Zielordner kann nicht erstellt werden</string>
|
<string name="error_path_creation">Der Zielordner kann nicht erstellt werden</string>
|
||||||
<string name="error_permission_denied">System verweigert den Zugriff</string>
|
<string name="error_permission_denied">System verweigert den Zugriff</string>
|
||||||
<string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string>
|
<string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string>
|
||||||
<string name="error_unknown_host">Der Server konnte nicht gefunden werden</string>
|
<string name="error_unknown_host">Der Server konnte nicht gefunden werden</string>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<string name="share_dialog_title">Compartir con</string>
|
<string name="share_dialog_title">Compartir con</string>
|
||||||
<string name="choose_browser">Elegir navegador</string>
|
<string name="choose_browser">Elegir navegador</string>
|
||||||
<string name="screen_rotation">rotación</string>
|
<string name="screen_rotation">rotación</string>
|
||||||
<string name="download_path_title">Ruta de descarga de vídeo</string>
|
<string name="download_path_title">Carpeta de descarga de vídeo</string>
|
||||||
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
|
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
|
||||||
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
|
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
|
||||||
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
|
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
<string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string>
|
<string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string>
|
||||||
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
|
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
|
||||||
<string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string>
|
<string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string>
|
||||||
<string name="download_path_audio_summary">Los audios descargados se almacenan aquí</string>
|
<string name="download_path_audio_summary">Ruta para almacenar los audios descargados</string>
|
||||||
<string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string>
|
<string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string>
|
||||||
<string name="blocked_by_gema">Bloqueado por GEMA</string>
|
<string name="blocked_by_gema">Bloqueado por GEMA</string>
|
||||||
<string name="download_path_audio_title">Carpeta de descarga de audio</string>
|
<string name="download_path_audio_title">Carpeta de descarga de audio</string>
|
||||||
|
@ -418,7 +418,9 @@ abrir en modo popup</string>
|
||||||
<!-- dialog about existing downloads -->
|
<!-- dialog about existing downloads -->
|
||||||
<string name="generate_unique_name">Generar nombre único</string>
|
<string name="generate_unique_name">Generar nombre único</string>
|
||||||
<string name="overwrite">Sobrescribir</string>
|
<string name="overwrite">Sobrescribir</string>
|
||||||
<string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string>
|
<string name="overwrite_unrelated_warning">Ya existe un archivo con este nombre</string>
|
||||||
|
<string name="overwrite_finished_warning">Ya existe un archivo descargado con este nombre</string>
|
||||||
|
<string name="overwrite_failed">No se puede sobrescribir el archivo</string>
|
||||||
<string name="download_already_running">Hay una descarga en curso con este nombre</string>
|
<string name="download_already_running">Hay una descarga en curso con este nombre</string>
|
||||||
<string name="download_already_pending">Hay una descarga pendiente con este nombre</string>
|
<string name="download_already_pending">Hay una descarga pendiente con este nombre</string>
|
||||||
|
|
||||||
|
@ -440,8 +442,8 @@ abrir en modo popup</string>
|
||||||
<!-- message dialog about download error -->
|
<!-- message dialog about download error -->
|
||||||
<string name="show_error">Mostrar error</string>
|
<string name="show_error">Mostrar error</string>
|
||||||
<string name="label_code">Codigo</string>
|
<string name="label_code">Codigo</string>
|
||||||
<string name="error_path_creation">No se puede crear la carpeta de destino</string>
|
<string name="error_file_creation">No se puede crear la carpeta de destino</string>
|
||||||
<string name="error_file_creation">No se puede crear el archivo</string>
|
<string name="error_path_creation">No se puede crear el archivo</string>
|
||||||
<string name="error_permission_denied">Permiso denegado por el sistema</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_ssl_exception">Fallo la conexión segura</string>
|
||||||
<string name="error_unknown_host">No se pudo encontrar el servidor</string>
|
<string name="error_unknown_host">No se pudo encontrar el servidor</string>
|
||||||
|
@ -453,6 +455,19 @@ abrir en modo popup</string>
|
||||||
<string name="error_postprocessing_failed">Fallo el post-procesado</string>
|
<string name="error_postprocessing_failed">Fallo el post-procesado</string>
|
||||||
<string name="error_postprocessing_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string>
|
<string name="error_postprocessing_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string>
|
||||||
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
|
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
|
||||||
|
<string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string>
|
||||||
|
|
||||||
|
<string name="downloads_storage">API de almacenamiento</string>
|
||||||
|
<string name="downloads_storage_desc">Seleccione que API utilizar para almacenar las descargas</string>
|
||||||
|
|
||||||
|
<string name="storage_access_framework_description">Framework de acceso a almacenamiento</string>
|
||||||
|
<string name="java_io_description">Java I/O</string>
|
||||||
|
|
||||||
|
<string name="save_as">Guardar como…</string>
|
||||||
|
|
||||||
|
<string name="download_to_sdcard_error_message">No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\?</string>
|
||||||
|
|
||||||
|
<string name="download_pick_path">Seleccione los directorios de descarga</string>
|
||||||
|
|
||||||
<string name="unsubscribe">Desuscribirse</string>
|
<string name="unsubscribe">Desuscribirse</string>
|
||||||
<string name="tab_new">Nueva pestaña</string>
|
<string name="tab_new">Nueva pestaña</string>
|
||||||
|
|
|
@ -446,12 +446,12 @@
|
||||||
<string name="download_finished_more">%s deskarga amaituta</string>
|
<string name="download_finished_more">%s deskarga amaituta</string>
|
||||||
<string name="generate_unique_name">Sortu izen bakana</string>
|
<string name="generate_unique_name">Sortu izen bakana</string>
|
||||||
<string name="overwrite">Gainidatzi</string>
|
<string name="overwrite">Gainidatzi</string>
|
||||||
<string name="overwrite_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
|
<string name="overwrite_finished_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
|
||||||
<string name="download_already_running">Badago izen bera duen deskarga bat abian</string>
|
<string name="download_already_running">Badago izen bera duen deskarga bat abian</string>
|
||||||
<string name="show_error">Erakutsi errorea</string>
|
<string name="show_error">Erakutsi errorea</string>
|
||||||
<string name="label_code">Kodea</string>
|
<string name="label_code">Kodea</string>
|
||||||
<string name="error_path_creation">Ezin da fitxategia sortu</string>
|
<string name="error_file_creation">Ezin da fitxategia sortu</string>
|
||||||
<string name="error_file_creation">Ezin da helburu karpeta sortu</string>
|
<string name="error_path_creation">Ezin da helburu karpeta sortu</string>
|
||||||
<string name="error_permission_denied">Sistemak baimena ukatu du</string>
|
<string name="error_permission_denied">Sistemak baimena ukatu du</string>
|
||||||
<string name="error_ssl_exception">Konexio seguruak huts egin du</string>
|
<string name="error_ssl_exception">Konexio seguruak huts egin du</string>
|
||||||
<string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string>
|
<string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string>
|
||||||
|
|
|
@ -451,12 +451,12 @@
|
||||||
<string name="download_finished_more">%s הורדות הסתיימו</string>
|
<string name="download_finished_more">%s הורדות הסתיימו</string>
|
||||||
<string name="generate_unique_name">יצירת שם ייחודי</string>
|
<string name="generate_unique_name">יצירת שם ייחודי</string>
|
||||||
<string name="overwrite">שכתוב</string>
|
<string name="overwrite">שכתוב</string>
|
||||||
<string name="overwrite_warning">כבר קיים קובץ בשם הזה</string>
|
<string name="overwrite_finished_warning">כבר קיים קובץ בשם הזה</string>
|
||||||
<string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string>
|
<string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string>
|
||||||
<string name="show_error">הצגת שגיאה</string>
|
<string name="show_error">הצגת שגיאה</string>
|
||||||
<string name="label_code">קוד</string>
|
<string name="label_code">קוד</string>
|
||||||
<string name="error_path_creation">לא ניתן ליצור את הקובץ</string>
|
<string name="error_file_creation">לא ניתן ליצור את הקובץ</string>
|
||||||
<string name="error_file_creation">לא ניתן ליצור את תיקיית היעד</string>
|
<string name="error_path_creation">לא ניתן ליצור את תיקיית היעד</string>
|
||||||
<string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string>
|
<string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string>
|
||||||
<string name="error_ssl_exception">החיבור המאובטח נכשל</string>
|
<string name="error_ssl_exception">החיבור המאובטח נכשל</string>
|
||||||
<string name="error_unknown_host">לא ניתן למצוא את השרת</string>
|
<string name="error_unknown_host">לא ניתן למצוא את השרת</string>
|
||||||
|
|
|
@ -447,12 +447,12 @@
|
||||||
<string name="download_finished_more">%s unduhan selesai</string>
|
<string name="download_finished_more">%s unduhan selesai</string>
|
||||||
<string name="generate_unique_name">Hasilkan nama unik</string>
|
<string name="generate_unique_name">Hasilkan nama unik</string>
|
||||||
<string name="overwrite">Timpa</string>
|
<string name="overwrite">Timpa</string>
|
||||||
<string name="overwrite_warning">File yang diunduh dengan nama ini sudah ada</string>
|
<string name="overwrite_finished_warning">File yang diunduh dengan nama ini sudah ada</string>
|
||||||
<string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string>
|
<string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string>
|
||||||
<string name="show_error">Tunjukkan kesalahan</string>
|
<string name="show_error">Tunjukkan kesalahan</string>
|
||||||
<string name="label_code">Kode</string>
|
<string name="label_code">Kode</string>
|
||||||
<string name="error_path_creation">File tidak dapat dibuat</string>
|
<string name="error_file_creation">File tidak dapat dibuat</string>
|
||||||
<string name="error_file_creation">Folder tujuan tidak dapat dibuat</string>
|
<string name="error_path_creation">Folder tujuan tidak dapat dibuat</string>
|
||||||
<string name="error_permission_denied">Izin ditolak oleh sistem</string>
|
<string name="error_permission_denied">Izin ditolak oleh sistem</string>
|
||||||
<string name="error_ssl_exception">Koneksi aman gagal</string>
|
<string name="error_ssl_exception">Koneksi aman gagal</string>
|
||||||
<string name="error_unknown_host">Tidak dapat menemukan server</string>
|
<string name="error_unknown_host">Tidak dapat menemukan server</string>
|
||||||
|
|
|
@ -449,12 +449,12 @@
|
||||||
<string name="download_finished_more">%s download finiti</string>
|
<string name="download_finished_more">%s download finiti</string>
|
||||||
<string name="generate_unique_name">Genera un nome unico</string>
|
<string name="generate_unique_name">Genera un nome unico</string>
|
||||||
<string name="overwrite">Sovrascrivi</string>
|
<string name="overwrite">Sovrascrivi</string>
|
||||||
<string name="overwrite_warning">Esiste già un file scaricato con lo stesso nome</string>
|
<string name="overwrite_finished_warning">Esiste già un file scaricato con lo stesso nome</string>
|
||||||
<string name="download_already_running">C\'è un download in progresso con questo nome</string>
|
<string name="download_already_running">C\'è un download in progresso con questo nome</string>
|
||||||
<string name="show_error">Mostra errore</string>
|
<string name="show_error">Mostra errore</string>
|
||||||
<string name="label_code">Codice</string>
|
<string name="label_code">Codice</string>
|
||||||
<string name="error_path_creation">Impossibile creare il file</string>
|
<string name="error_file_creation">Impossibile creare il file</string>
|
||||||
<string name="error_file_creation">Impossibile creare la cartella di destinazione</string>
|
<string name="error_path_creation">Impossibile creare la cartella di destinazione</string>
|
||||||
<string name="error_permission_denied">Permesso negato dal sistema</string>
|
<string name="error_permission_denied">Permesso negato dal sistema</string>
|
||||||
<string name="error_ssl_exception">Connessione sicura fallita</string>
|
<string name="error_ssl_exception">Connessione sicura fallita</string>
|
||||||
<string name="error_unknown_host">Impossibile trovare il server</string>
|
<string name="error_unknown_host">Impossibile trovare il server</string>
|
||||||
|
|
|
@ -365,10 +365,10 @@
|
||||||
<string name="caption_auto_generated">自動生成</string>
|
<string name="caption_auto_generated">自動生成</string>
|
||||||
<string name="caption_setting_description">アプリの再起動後、設定した字幕設定が反映されます</string>
|
<string name="caption_setting_description">アプリの再起動後、設定した字幕設定が反映されます</string>
|
||||||
<string name="empty_subscription_feed_subtitle">何もありません</string>
|
<string name="empty_subscription_feed_subtitle">何もありません</string>
|
||||||
<string name="import_youtube_instructions">保存したエクスポートファイルからYouTubeの購読をインポート:
|
<string name="import_youtube_instructions">保存したエクスポートファイルからYouTubeの購読をインポート:
|
||||||
\n
|
\n
|
||||||
\n1. このURLを開きます: %1$s
|
\n1. このURLを開きます: %1$s
|
||||||
\n2. ログインしていなければログインします
|
\n2. ログインしていなければログインします
|
||||||
\n3. ダウンロードが始まります (これがエクスポートファイルです)</string>
|
\n3. ダウンロードが始まります (これがエクスポートファイルです)</string>
|
||||||
<string name="playback_reset">リセット</string>
|
<string name="playback_reset">リセット</string>
|
||||||
<string name="accept">同意する</string>
|
<string name="accept">同意する</string>
|
||||||
|
@ -391,8 +391,8 @@
|
||||||
\n3. 必要に応じてログインします
|
\n3. 必要に応じてログインします
|
||||||
\n4. リダイレクトされたプロファイル URL をコピーします。</string>
|
\n4. リダイレクトされたプロファイル URL をコピーします。</string>
|
||||||
<string name="import_soundcloud_instructions_hint">あなたのID, soundcloud.com/あなたのid</string>
|
<string name="import_soundcloud_instructions_hint">あなたのID, soundcloud.com/あなたのid</string>
|
||||||
<string name="import_network_expensive_warning">この操作により通信料金が増えることがあります。ご注意ください。
|
<string name="import_network_expensive_warning">この操作により通信料金が増えることがあります。ご注意ください。
|
||||||
\n
|
\n
|
||||||
\n続行しますか\?</string>
|
\n続行しますか\?</string>
|
||||||
<string name="playback_speed_control">再生速度を変更</string>
|
<string name="playback_speed_control">再生速度を変更</string>
|
||||||
<string name="unhook_checkbox">速度と音程を連動せずに変更 (歪むかもしれません)</string>
|
<string name="unhook_checkbox">速度と音程を連動せずに変更 (歪むかもしれません)</string>
|
||||||
|
|
|
@ -443,12 +443,12 @@
|
||||||
<string name="download_finished_more">%s muat turun selesai</string>
|
<string name="download_finished_more">%s muat turun selesai</string>
|
||||||
<string name="generate_unique_name">Menjana nama yang unik</string>
|
<string name="generate_unique_name">Menjana nama yang unik</string>
|
||||||
<string name="overwrite">Timpa</string>
|
<string name="overwrite">Timpa</string>
|
||||||
<string name="overwrite_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
|
<string name="overwrite_finished_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
|
||||||
<string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string>
|
<string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string>
|
||||||
<string name="show_error">Tunjukkan kesilapan</string>
|
<string name="show_error">Tunjukkan kesilapan</string>
|
||||||
<string name="label_code">Kod</string>
|
<string name="label_code">Kod</string>
|
||||||
<string name="error_path_creation">Fail tidak boleh dibuat</string>
|
<string name="error_file_creation">Fail tidak boleh dibuat</string>
|
||||||
<string name="error_file_creation">Folder destinasi tidak boleh dibuat</string>
|
<string name="error_path_creation">Folder destinasi tidak boleh dibuat</string>
|
||||||
<string name="error_permission_denied">Kebenaran ditolak oleh sistem</string>
|
<string name="error_permission_denied">Kebenaran ditolak oleh sistem</string>
|
||||||
<string name="error_ssl_exception">Sambungan selamat gagal</string>
|
<string name="error_ssl_exception">Sambungan selamat gagal</string>
|
||||||
<string name="error_unknown_host">Tidak dapat mencari server</string>
|
<string name="error_unknown_host">Tidak dapat mencari server</string>
|
||||||
|
|
|
@ -526,12 +526,12 @@
|
||||||
<string name="download_finished_more">%s nedlastinger fullført</string>
|
<string name="download_finished_more">%s nedlastinger fullført</string>
|
||||||
<string name="generate_unique_name">Generer unikt navn</string>
|
<string name="generate_unique_name">Generer unikt navn</string>
|
||||||
<string name="overwrite">Overskriv</string>
|
<string name="overwrite">Overskriv</string>
|
||||||
<string name="overwrite_warning">Nedlastet fil ved dette navnet finnes allerede</string>
|
<string name="overwrite_finished_warning">Nedlastet fil ved dette navnet finnes allerede</string>
|
||||||
<string name="download_already_running">Nedlasting med dette navnet underveis allerede</string>
|
<string name="download_already_running">Nedlasting med dette navnet underveis allerede</string>
|
||||||
<string name="show_error">Vis feil</string>
|
<string name="show_error">Vis feil</string>
|
||||||
<string name="label_code">Kode</string>
|
<string name="label_code">Kode</string>
|
||||||
<string name="error_path_creation">Filen kan ikke opprettes</string>
|
<string name="error_file_creation">Filen kan ikke opprettes</string>
|
||||||
<string name="error_file_creation">Målmappen kan ikke opprettes</string>
|
<string name="error_path_creation">Målmappen kan ikke opprettes</string>
|
||||||
<string name="error_permission_denied">Tilgang nektet av systemet</string>
|
<string name="error_permission_denied">Tilgang nektet av systemet</string>
|
||||||
<string name="error_ssl_exception">Sikker tilkobling mislyktes</string>
|
<string name="error_ssl_exception">Sikker tilkobling mislyktes</string>
|
||||||
<string name="error_unknown_host">Fant ikke tjeneren</string>
|
<string name="error_unknown_host">Fant ikke tjeneren</string>
|
||||||
|
|
|
@ -445,12 +445,12 @@
|
||||||
<string name="download_finished_more">%s downloads voltooid</string>
|
<string name="download_finished_more">%s downloads voltooid</string>
|
||||||
<string name="generate_unique_name">Unieke naam genereren</string>
|
<string name="generate_unique_name">Unieke naam genereren</string>
|
||||||
<string name="overwrite">Overschrijven</string>
|
<string name="overwrite">Overschrijven</string>
|
||||||
<string name="overwrite_warning">Der bestaat al een gedownload bestand met deze naam</string>
|
<string name="overwrite_finished_warning">Der bestaat al een gedownload bestand met deze naam</string>
|
||||||
<string name="download_already_running">Der is al een download met deze naam bezig</string>
|
<string name="download_already_running">Der is al een download met deze naam bezig</string>
|
||||||
<string name="show_error">Foutmelding weergeven</string>
|
<string name="show_error">Foutmelding weergeven</string>
|
||||||
<string name="label_code">Code</string>
|
<string name="label_code">Code</string>
|
||||||
<string name="error_path_creation">Het bestand kan niet aangemaakt worden</string>
|
<string name="error_file_creation">Het bestand kan niet aangemaakt worden</string>
|
||||||
<string name="error_file_creation">De doelmap kan niet aangemaakt worden</string>
|
<string name="error_path_creation">De doelmap kan niet aangemaakt worden</string>
|
||||||
<string name="error_permission_denied">Toelating geweigerd door het systeem</string>
|
<string name="error_permission_denied">Toelating geweigerd door het systeem</string>
|
||||||
<string name="error_ssl_exception">Beveiligde verbinding is mislukt</string>
|
<string name="error_ssl_exception">Beveiligde verbinding is mislukt</string>
|
||||||
<string name="error_unknown_host">Kon de server niet vinden</string>
|
<string name="error_unknown_host">Kon de server niet vinden</string>
|
||||||
|
|
|
@ -449,12 +449,12 @@
|
||||||
<string name="download_finished_more">%s downloads voltooid</string>
|
<string name="download_finished_more">%s downloads voltooid</string>
|
||||||
<string name="generate_unique_name">Genereer een unieke naam</string>
|
<string name="generate_unique_name">Genereer een unieke naam</string>
|
||||||
<string name="overwrite">Overschrijven</string>
|
<string name="overwrite">Overschrijven</string>
|
||||||
<string name="overwrite_warning">Er bestaat al een gedownload bestand met deze naam</string>
|
<string name="overwrite_finished_warning">Er bestaat al een gedownload bestand met deze naam</string>
|
||||||
<string name="download_already_running">Er is een download aan de gang met deze naam</string>
|
<string name="download_already_running">Er is een download aan de gang met deze naam</string>
|
||||||
<string name="show_error">Toon foutmelding</string>
|
<string name="show_error">Toon foutmelding</string>
|
||||||
<string name="label_code">Code</string>
|
<string name="label_code">Code</string>
|
||||||
<string name="error_path_creation">Het bestand kan niet worden gemaakt</string>
|
<string name="error_file_creation">Het bestand kan niet worden gemaakt</string>
|
||||||
<string name="error_file_creation">De doelmap kan niet worden gemaakt</string>
|
<string name="error_path_creation">De doelmap kan niet worden gemaakt</string>
|
||||||
<string name="error_permission_denied">Toestemming door het systeem geweigerd</string>
|
<string name="error_permission_denied">Toestemming door het systeem geweigerd</string>
|
||||||
<string name="error_ssl_exception">Beveiligde connectie is mislukt</string>
|
<string name="error_ssl_exception">Beveiligde connectie is mislukt</string>
|
||||||
<string name="error_unknown_host">Kon de server niet vinden</string>
|
<string name="error_unknown_host">Kon de server niet vinden</string>
|
||||||
|
|
|
@ -446,12 +446,12 @@
|
||||||
<string name="download_finished_more">%s pobieranie zostało zakończone</string>
|
<string name="download_finished_more">%s pobieranie zostało zakończone</string>
|
||||||
<string name="generate_unique_name">Wygeneruj unikalną nazwę</string>
|
<string name="generate_unique_name">Wygeneruj unikalną nazwę</string>
|
||||||
<string name="overwrite">Zastąp</string>
|
<string name="overwrite">Zastąp</string>
|
||||||
<string name="overwrite_warning">Pobrany plik o tej nazwie już istnieje</string>
|
<string name="overwrite_finished_warning">Pobrany plik o tej nazwie już istnieje</string>
|
||||||
<string name="download_already_running">Trwa pobieranie z tą nazwą</string>
|
<string name="download_already_running">Trwa pobieranie z tą nazwą</string>
|
||||||
<string name="show_error">Pokaż błąd</string>
|
<string name="show_error">Pokaż błąd</string>
|
||||||
<string name="label_code">Kod</string>
|
<string name="label_code">Kod</string>
|
||||||
<string name="error_path_creation">Nie można utworzyć pliku</string>
|
<string name="error_file_creation">Nie można utworzyć pliku</string>
|
||||||
<string name="error_file_creation">Nie można utworzyć folderu docelowego</string>
|
<string name="error_path_creation">Nie można utworzyć folderu docelowego</string>
|
||||||
<string name="error_permission_denied">Odmowa dostępu do systemu</string>
|
<string name="error_permission_denied">Odmowa dostępu do systemu</string>
|
||||||
<string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string>
|
<string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string>
|
||||||
<string name="error_unknown_host">Nie można znaleźć serwera</string>
|
<string name="error_unknown_host">Nie można znaleźć serwera</string>
|
||||||
|
|
|
@ -446,12 +446,12 @@ abrir em modo popup</string>
|
||||||
<string name="download_finished_more">%s downloads terminados</string>
|
<string name="download_finished_more">%s downloads terminados</string>
|
||||||
<string name="generate_unique_name">Gerar nome único</string>
|
<string name="generate_unique_name">Gerar nome único</string>
|
||||||
<string name="overwrite">"Sobrescrever "</string>
|
<string name="overwrite">"Sobrescrever "</string>
|
||||||
<string name="overwrite_warning">Um arquivo baixado com esse nome já existe</string>
|
<string name="overwrite_finished_warning">Um arquivo baixado com esse nome já existe</string>
|
||||||
<string name="download_already_running">Existe um download em progresso com esse nome</string>
|
<string name="download_already_running">Existe um download em progresso com esse nome</string>
|
||||||
<string name="show_error">Mostrar erro</string>
|
<string name="show_error">Mostrar erro</string>
|
||||||
<string name="label_code">Código</string>
|
<string name="label_code">Código</string>
|
||||||
<string name="error_path_creation">O arquivo não pode ser criado</string>
|
<string name="error_file_creation">O arquivo não pode ser criado</string>
|
||||||
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
|
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
|
||||||
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
||||||
<string name="error_ssl_exception">"Falha na conexão segura "</string>
|
<string name="error_ssl_exception">"Falha na conexão segura "</string>
|
||||||
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
||||||
|
|
|
@ -442,12 +442,12 @@
|
||||||
<string name="download_finished_more">%s descargas terminadas</string>
|
<string name="download_finished_more">%s descargas terminadas</string>
|
||||||
<string name="generate_unique_name">Gerar nome único</string>
|
<string name="generate_unique_name">Gerar nome único</string>
|
||||||
<string name="overwrite">Sobrescrever</string>
|
<string name="overwrite">Sobrescrever</string>
|
||||||
<string name="overwrite_warning">Um ficheiro descarregado com este nome já existe</string>
|
<string name="overwrite_finished_warning">Um ficheiro descarregado com este nome já existe</string>
|
||||||
<string name="download_already_running">Já existe uma descarga em curso com este nome</string>
|
<string name="download_already_running">Já existe uma descarga em curso com este nome</string>
|
||||||
<string name="show_error">Mostrar erro</string>
|
<string name="show_error">Mostrar erro</string>
|
||||||
<string name="label_code">Código</string>
|
<string name="label_code">Código</string>
|
||||||
<string name="error_path_creation">O ficheiro não pode ser criado</string>
|
<string name="error_file_creation">O ficheiro não pode ser criado</string>
|
||||||
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
|
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
|
||||||
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
||||||
<string name="error_ssl_exception">Ligação segura falhou</string>
|
<string name="error_ssl_exception">Ligação segura falhou</string>
|
||||||
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
||||||
|
|
|
@ -442,12 +442,12 @@
|
||||||
<string name="permission_denied">Действие запрещено системой</string>
|
<string name="permission_denied">Действие запрещено системой</string>
|
||||||
<string name="download_failed">Ошибка загрузки</string>
|
<string name="download_failed">Ошибка загрузки</string>
|
||||||
<string name="overwrite">Перезаписать</string>
|
<string name="overwrite">Перезаписать</string>
|
||||||
<string name="overwrite_warning">Файл с таким именем уже существует</string>
|
<string name="overwrite_finished_warning">Файл с таким именем уже существует</string>
|
||||||
<string name="download_already_running">Загрузка с таким именем уже выполняется</string>
|
<string name="download_already_running">Загрузка с таким именем уже выполняется</string>
|
||||||
<string name="show_error">Показать текст ошибки</string>
|
<string name="show_error">Показать текст ошибки</string>
|
||||||
<string name="label_code">Код</string>
|
<string name="label_code">Код</string>
|
||||||
<string name="error_path_creation">Файл не может быть создан</string>
|
<string name="error_path_creation">Папка назначения не может быть создана</string>
|
||||||
<string name="error_file_creation">Папка назначения не может быть создана</string>
|
<string name="error_file_creation">Файл не может быть создан</string>
|
||||||
<string name="error_permission_denied">Доступ запрещен системой</string>
|
<string name="error_permission_denied">Доступ запрещен системой</string>
|
||||||
<string name="error_unknown_host">Сервер не найден</string>
|
<string name="error_unknown_host">Сервер не найден</string>
|
||||||
<string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1"</string>
|
<string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1"</string>
|
||||||
|
|
|
@ -449,12 +449,12 @@
|
||||||
<string name="download_finished_more">%s indirme bitti</string>
|
<string name="download_finished_more">%s indirme bitti</string>
|
||||||
<string name="generate_unique_name">Benzersiz ad oluştur</string>
|
<string name="generate_unique_name">Benzersiz ad oluştur</string>
|
||||||
<string name="overwrite">Üzerine yaz</string>
|
<string name="overwrite">Üzerine yaz</string>
|
||||||
<string name="overwrite_warning">Bu ada sahip indirilen bir dosya zaten var</string>
|
<string name="overwrite_finished_warning">Bu ada sahip indirilen bir dosya zaten var</string>
|
||||||
<string name="download_already_running">Bu ad ile devam eden bir indirme var</string>
|
<string name="download_already_running">Bu ad ile devam eden bir indirme var</string>
|
||||||
<string name="show_error">Hatayı göster</string>
|
<string name="show_error">Hatayı göster</string>
|
||||||
<string name="label_code">Kod</string>
|
<string name="label_code">Kod</string>
|
||||||
<string name="error_path_creation">Dosya oluşturulamıyor</string>
|
<string name="error_file_creation">Dosya oluşturulamıyor</string>
|
||||||
<string name="error_file_creation">Hedef klasör oluşturulamıyor</string>
|
<string name="error_path_creation">Hedef klasör oluşturulamıyor</string>
|
||||||
<string name="error_permission_denied">İzin sistem tarafından reddedildi</string>
|
<string name="error_permission_denied">İzin sistem tarafından reddedildi</string>
|
||||||
<string name="error_ssl_exception">Güvenli bağlantı başarısız</string>
|
<string name="error_ssl_exception">Güvenli bağlantı başarısız</string>
|
||||||
<string name="error_unknown_host">Sunucu bulunamadı</string>
|
<string name="error_unknown_host">Sunucu bulunamadı</string>
|
||||||
|
|
|
@ -440,11 +440,11 @@
|
||||||
<string name="download_finished_more">%s tải về đã xong</string>
|
<string name="download_finished_more">%s tải về đã xong</string>
|
||||||
<string name="generate_unique_name">Tạo tên riêng biệt</string>
|
<string name="generate_unique_name">Tạo tên riêng biệt</string>
|
||||||
<string name="overwrite">Ghi đè</string>
|
<string name="overwrite">Ghi đè</string>
|
||||||
<string name="overwrite_warning">Có một tệp đã tải về trùng tên</string>
|
<string name="overwrite_finished_warning">Có một tệp đã tải về trùng tên</string>
|
||||||
<string name="download_already_running">Có một tệp trùng tên đang tải về</string>
|
<string name="download_already_running">Có một tệp trùng tên đang tải về</string>
|
||||||
<string name="show_error">Hiện lỗi</string>
|
<string name="show_error">Hiện lỗi</string>
|
||||||
<string name="error_path_creation">Không thể tạo tệp</string>
|
<string name="error_file_creation">Không thể tạo tệp</string>
|
||||||
<string name="error_file_creation">Không thể tạo thư mục đích</string>
|
<string name="error_path_creation">Không thể tạo thư mục đích</string>
|
||||||
<string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string>
|
<string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string>
|
||||||
<string name="error_ssl_exception">Không thể tạo kết nối an toàn</string>
|
<string name="error_ssl_exception">Không thể tạo kết nối an toàn</string>
|
||||||
<string name="error_unknown_host">Không thể tìm máy chủ</string>
|
<string name="error_unknown_host">Không thể tìm máy chủ</string>
|
||||||
|
|
|
@ -445,12 +445,12 @@
|
||||||
<string name="download_finished_more">%s 個下載已結束</string>
|
<string name="download_finished_more">%s 個下載已結束</string>
|
||||||
<string name="generate_unique_name">生成獨特的名稱</string>
|
<string name="generate_unique_name">生成獨特的名稱</string>
|
||||||
<string name="overwrite">覆寫</string>
|
<string name="overwrite">覆寫</string>
|
||||||
<string name="overwrite_warning">已有此名稱的已下載檔案</string>
|
<string name="overwrite_finished_warning">已有此名稱的已下載檔案</string>
|
||||||
<string name="download_already_running">已有此名稱的當案的下載正在進行</string>
|
<string name="download_already_running">已有此名稱的當案的下載正在進行</string>
|
||||||
<string name="show_error">顯示錯誤</string>
|
<string name="show_error">顯示錯誤</string>
|
||||||
<string name="label_code">代碼</string>
|
<string name="label_code">代碼</string>
|
||||||
<string name="error_path_creation">無法建立檔案</string>
|
<string name="error_file_creation">無法建立檔案</string>
|
||||||
<string name="error_file_creation">無法建立目的地資料夾</string>
|
<string name="error_path_creation">無法建立目的地資料夾</string>
|
||||||
<string name="error_permission_denied">被系統拒絕的權限</string>
|
<string name="error_permission_denied">被系統拒絕的權限</string>
|
||||||
<string name="error_ssl_exception">安全連線失敗</string>
|
<string name="error_ssl_exception">安全連線失敗</string>
|
||||||
<string name="error_unknown_host">找不到伺服器</string>
|
<string name="error_unknown_host">找不到伺服器</string>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
|
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
|
||||||
|
|
||||||
<!-- Key values -->
|
<!-- Key values -->
|
||||||
<string name="download_path_key" translatable="false">download_path</string>
|
<string name="download_path_video_key" translatable="false">download_path</string>
|
||||||
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
|
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
|
||||||
|
|
||||||
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
|
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
|
||||||
|
@ -160,6 +160,21 @@
|
||||||
<string name="clear_views_history_key" translatable="false">clear_play_history</string>
|
<string name="clear_views_history_key" translatable="false">clear_play_history</string>
|
||||||
<string name="clear_search_history_key" translatable="false">clear_search_history</string>
|
<string name="clear_search_history_key" translatable="false">clear_search_history</string>
|
||||||
|
|
||||||
|
<string name="downloads_storage_api" translatable="false">downloads_storage_api</string>
|
||||||
|
|
||||||
|
<!-- WARNING: changing the default value will require update the code too -->
|
||||||
|
<string name="downloads_storage_api_default" translatable="false">javaIO</string>
|
||||||
|
|
||||||
|
<string-array name="downloads_storage_api_values" translatable="false">
|
||||||
|
<item translatable="false">SAF</item>
|
||||||
|
<item translatable="false">javaIO</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="downloads_storage_api_description" translatable="true">
|
||||||
|
<item translatable="true">@string/storage_access_framework_description</item>
|
||||||
|
<item translatable="true">@string/java_io_description</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<!-- FileName Downloads -->
|
<!-- FileName Downloads -->
|
||||||
<string name="settings_file_charset_key" translatable="false">file_rename</string>
|
<string name="settings_file_charset_key" translatable="false">file_rename</string>
|
||||||
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
|
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
|
||||||
|
|
|
@ -176,7 +176,7 @@
|
||||||
<!-- error strings -->
|
<!-- error strings -->
|
||||||
<string name="general_error">Error</string>
|
<string name="general_error">Error</string>
|
||||||
<string name="download_to_sdcard_error_title">External storage unavailable</string>
|
<string name="download_to_sdcard_error_title">External storage unavailable</string>
|
||||||
<string name="download_to_sdcard_error_message">Downloading to external SD card not yet possible. Reset download folder location\?</string>
|
<string name="download_to_sdcard_error_message">Downloading to external SD card not possible. Reset download folder location\?</string>
|
||||||
<string name="network_error">Network error</string>
|
<string name="network_error">Network error</string>
|
||||||
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
|
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
|
||||||
<string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string>
|
<string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string>
|
||||||
|
@ -512,15 +512,17 @@
|
||||||
<!-- dialog about existing downloads -->
|
<!-- dialog about existing downloads -->
|
||||||
<string name="generate_unique_name">Generate unique name</string>
|
<string name="generate_unique_name">Generate unique name</string>
|
||||||
<string name="overwrite">Overwrite</string>
|
<string name="overwrite">Overwrite</string>
|
||||||
<string name="overwrite_warning">A downloaded file with this name already exists</string>
|
<string name="overwrite_unrelated_warning">A file with this name already exists</string>
|
||||||
|
<string name="overwrite_finished_warning">A downloaded file with this name already exists</string>
|
||||||
|
<string name="overwrite_failed">cannot overwrite the file</string>
|
||||||
<string name="download_already_running">There is a download in progress with this name</string>
|
<string name="download_already_running">There is a download in progress with this name</string>
|
||||||
<string name="download_already_pending">There is a pending download with this name</string>
|
<string name="download_already_pending">There is a pending download with this name</string>
|
||||||
|
|
||||||
<!-- message dialog about download error -->
|
<!-- message dialog about download error -->
|
||||||
<string name="show_error">Show error</string>
|
<string name="show_error">Show error</string>
|
||||||
<string name="label_code">Code</string>
|
<string name="label_code">Code</string>
|
||||||
<string name="error_path_creation">The file can not be created</string>
|
<string name="error_file_creation">The file can not be created</string>
|
||||||
<string name="error_file_creation">The destination folder can not be created</string>
|
<string name="error_path_creation">The destination folder can not be created</string>
|
||||||
<string name="error_permission_denied">Permission denied by the system</string>
|
<string name="error_permission_denied">Permission denied by the system</string>
|
||||||
<string name="error_ssl_exception">Secure connection failed</string>
|
<string name="error_ssl_exception">Secure connection failed</string>
|
||||||
<string name="error_unknown_host">Could not find the server</string>
|
<string name="error_unknown_host">Could not find the server</string>
|
||||||
|
@ -532,6 +534,7 @@
|
||||||
<string name="error_postprocessing_failed">Post-processing failed</string>
|
<string name="error_postprocessing_failed">Post-processing failed</string>
|
||||||
<string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string>
|
<string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string>
|
||||||
<string name="error_insufficient_storage">No space left on device</string>
|
<string name="error_insufficient_storage">No space left on device</string>
|
||||||
|
<string name="error_progress_lost">Progress lost, because the file was deleted</string>
|
||||||
|
|
||||||
<string name="clear_finished_download">Clear finished downloads</string>
|
<string name="clear_finished_download">Clear finished downloads</string>
|
||||||
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
|
<string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string>
|
||||||
|
@ -546,4 +549,14 @@
|
||||||
<string name="start_downloads">Start downloads</string>
|
<string name="start_downloads">Start downloads</string>
|
||||||
<string name="pause_downloads">Pause downloads</string>
|
<string name="pause_downloads">Pause downloads</string>
|
||||||
|
|
||||||
|
<string name="downloads_storage">Storage API</string>
|
||||||
|
<string name="downloads_storage_desc">Select which API use to store the downloads</string>
|
||||||
|
|
||||||
|
<string name="storage_access_framework_description">Storage Access Framework</string>
|
||||||
|
<string name="java_io_description">Java I/O</string>
|
||||||
|
|
||||||
|
<string name="save_as">Save as…</string>
|
||||||
|
|
||||||
|
<string name="download_pick_path">Select the downloads save path</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -4,10 +4,26 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:title="@string/settings_category_downloads_title">
|
android:title="@string/settings_category_downloads_title">
|
||||||
|
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
android:key="saf_test"
|
||||||
|
android:summary="Realiza una prueba del Storage Access Framework de Android"
|
||||||
|
android:title="Probar SAF"/>
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
android:defaultValue="@string/downloads_storage_api_default"
|
||||||
|
android:entries="@array/downloads_storage_api_description"
|
||||||
|
android:entryValues="@array/downloads_storage_api_values"
|
||||||
|
android:key="@string/downloads_storage_api"
|
||||||
|
android:summary="@string/downloads_storage_desc"
|
||||||
|
android:title="@string/downloads_storage" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
android:dialogTitle="@string/download_path_dialog_title"
|
android:dialogTitle="@string/download_path_dialog_title"
|
||||||
android:key="@string/download_path_key"
|
android:key="@string/download_path_video_key"
|
||||||
android:summary="@string/download_path_summary"
|
android:summary="@string/download_path_summary"
|
||||||
android:title="@string/download_path_title"/>
|
android:title="@string/download_path_title"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue